From 32cf1a03ab5725b1efd5cba0993c75d5e388cfa6 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Tue, 31 Oct 2023 19:45:32 +0000 Subject: [PATCH] Read current weather information from home assistant --- Cargo.lock | 29 +++++++++++++++++++++++++++++ assets/icons/README.md | 1 + assets/icons/clear-day.svg | 1 + assets/icons/clear-night.svg | 1 + assets/icons/cloud.svg | 1 + assets/icons/extreme-rain.svg | 1 + assets/icons/fog.svg | 1 + assets/icons/hail.svg | 1 + assets/icons/partly-cloudy-day.svg | 1 + assets/icons/partly-cloudy-night.svg | 1 + assets/icons/rain.svg | 1 + assets/icons/snow.svg | 1 + assets/icons/thunderstorms-rain.svg | 1 + assets/icons/thunderstorms.svg | 1 + assets/icons/wind.svg | 1 + shalom/Cargo.toml | 1 + shalom/src/hass_client.rs | 303 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------------------- shalom/src/oracle.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- shalom/src/pages/omni.rs | 22 ++++++++++++---------- shalom/src/theme.rs | 50 ++++++++++++++++++++++++++++++++++++++------------ shalom/src/widgets/cards/mod.rs | 1 + shalom/src/widgets/cards/weather.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ shalom/src/widgets/mod.rs | 1 + 23 files changed, 545 insertions(+), 106 deletions(-) create mode 100644 assets/icons/clear-day.svg create mode 100644 assets/icons/clear-night.svg create mode 100644 assets/icons/cloud.svg create mode 100644 assets/icons/extreme-rain.svg create mode 100644 assets/icons/fog.svg create mode 100644 assets/icons/hail.svg create mode 100644 assets/icons/partly-cloudy-day.svg create mode 100644 assets/icons/partly-cloudy-night.svg create mode 100644 assets/icons/rain.svg create mode 100644 assets/icons/snow.svg create mode 100644 assets/icons/thunderstorms-rain.svg create mode 100644 assets/icons/thunderstorms.svg create mode 100644 assets/icons/wind.svg create mode 100644 shalom/src/widgets/cards/mod.rs create mode 100644 shalom/src/widgets/cards/weather.rs diff --git a/Cargo.lock b/Cargo.lock index 5a73ab8..c2e1179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2326,6 +2326,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] name = "rustybuzz" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2452,6 +2458,7 @@ dependencies = [ "once_cell", "serde", "serde_json", + "strum", "time", "tokio", "tokio-tungstenite", @@ -2618,6 +2625,28 @@ dependencies = [ ] [[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.38", +] + +[[package]] name = "svg_fmt" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/assets/icons/README.md b/assets/icons/README.md index 2aa9bfb..ad6e55f 100644 --- a/assets/icons/README.md +++ b/assets/icons/README.md @@ -1,3 +1,4 @@ # Resources - https://heroicons.com/ +- https://github.com/basmilius/weather-icons diff --git a/assets/icons/clear-day.svg b/assets/icons/clear-day.svg new file mode 100644 index 0000000..bd486fa --- /dev/null +++ b/assets/icons/clear-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/clear-night.svg b/assets/icons/clear-night.svg new file mode 100644 index 0000000..5f97fac --- /dev/null +++ b/assets/icons/clear-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/cloud.svg b/assets/icons/cloud.svg new file mode 100644 index 0000000..0f19f52 --- /dev/null +++ b/assets/icons/cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/extreme-rain.svg b/assets/icons/extreme-rain.svg new file mode 100644 index 0000000..f1ffc4b --- /dev/null +++ b/assets/icons/extreme-rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/fog.svg b/assets/icons/fog.svg new file mode 100644 index 0000000..09d9dff --- /dev/null +++ b/assets/icons/fog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/hail.svg b/assets/icons/hail.svg new file mode 100644 index 0000000..f58baac --- /dev/null +++ b/assets/icons/hail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/partly-cloudy-day.svg b/assets/icons/partly-cloudy-day.svg new file mode 100644 index 0000000..cdff303 --- /dev/null +++ b/assets/icons/partly-cloudy-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/partly-cloudy-night.svg b/assets/icons/partly-cloudy-night.svg new file mode 100644 index 0000000..1cfeb3a --- /dev/null +++ b/assets/icons/partly-cloudy-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/rain.svg b/assets/icons/rain.svg new file mode 100644 index 0000000..52912e9 --- /dev/null +++ b/assets/icons/rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/snow.svg b/assets/icons/snow.svg new file mode 100644 index 0000000..bf96c7b --- /dev/null +++ b/assets/icons/snow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/thunderstorms-rain.svg b/assets/icons/thunderstorms-rain.svg new file mode 100644 index 0000000..5d4a654 --- /dev/null +++ b/assets/icons/thunderstorms-rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/thunderstorms.svg b/assets/icons/thunderstorms.svg new file mode 100644 index 0000000..2f4ea47 --- /dev/null +++ b/assets/icons/thunderstorms.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/wind.svg b/assets/icons/wind.svg new file mode 100644 index 0000000..56d7854 --- /dev/null +++ b/assets/icons/wind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/shalom/Cargo.toml b/shalom/Cargo.toml index 9906bc1..8276f50 100644 --- a/shalom/Cargo.toml +++ b/shalom/Cargo.toml @@ -14,6 +14,7 @@ itertools = "0.11" keyframe = "1.1" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } +strum = { version = "0.25", features = ["derive"] } tokio = { version = "1.33", features = ["net", "sync", "rt", "macros", "time", "fs"] } tokio-tungstenite = "0.20" toml = "0.8" diff --git a/shalom/src/hass_client.rs b/shalom/src/hass_client.rs index 84f4e05..8fd2d4a 100644 --- a/shalom/src/hass_client.rs +++ b/shalom/src/hass_client.rs @@ -175,11 +175,22 @@ impl HassRequest { } pub mod responses { - use std::borrow::Cow; - - use serde::Deserialize; + use std::{ + borrow::Cow, + fmt::{Display, Formatter}, + }; + + use serde::{ + de, + de::{MapAccess, Visitor}, + Deserialize, Deserializer, + }; + use serde_json::value::RawValue; + use strum::EnumString; use yoke::Yokeable; + use crate::theme::Icon; + #[derive(Deserialize, Yokeable, Debug)] pub struct AreaRegistryList<'a>(#[serde(borrow)] pub Vec>); @@ -268,43 +279,105 @@ pub mod responses { pub unique_id: Option>, } - #[derive(Deserialize, Yokeable, Debug)] - pub struct StatesList<'a>(#[serde(borrow)] pub Vec>); + #[derive(Yokeable, Debug, Deserialize)] + pub struct StatesList<'a>(#[serde(borrow, bound(deserialize = "'a: 'de"))] pub Vec>); - #[derive(Deserialize, Debug)] - pub enum State<'a> { - Sun { - #[serde(borrow)] - state: Cow<'a, str>, - attributes: StateSunAttributes, - }, - MediaPlayer { - #[serde(borrow)] - state: Cow<'a, str>, - #[serde(borrow)] - attributes: StateMediaPlayerAttributes<'a>, - }, - Camera { - #[serde(borrow)] - state: Cow<'a, str>, - #[serde(borrow)] - attributes: StateCameraAttributes<'a>, - }, - Weather { - #[serde(borrow)] - state: Cow<'a, str>, - #[serde(borrow)] - attributes: StateWeatherAttributes<'a>, - }, - Light { - #[serde(borrow)] - state: Cow<'a, str>, - #[serde(borrow)] - attributes: StateLightAttributes<'a>, + #[derive(Debug)] + pub struct State<'a> { + pub entity_id: Cow<'a, str>, + pub state: Cow<'a, str>, + pub attributes: StateAttributes<'a>, + } + + impl<'de> Deserialize<'de> for State<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_struct( + "State", + &["entity_id", "state", "attributes"], + StateVisitor {}, + ) + } + } + + pub struct StateVisitor {} + + impl<'de> Visitor<'de> for StateVisitor { + type Value = State<'de>; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("states struct") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut entity_id: Option> = None; + let mut state: Option> = None; + let mut attributes: Option<&'de RawValue> = None; + + while let Some(key) = map.next_key()? { + match key { + "entity_id" => { + entity_id = Some(map.next_value()?); + } + "state" => { + state = Some(map.next_value()?); + } + "attributes" => { + attributes = Some(map.next_value()?); + } + _ => { + let _: &'de RawValue = map.next_value()?; + } + } + } + + let entity_id = entity_id.ok_or_else(|| de::Error::missing_field("entity_id"))?; + let state = state.ok_or_else(|| de::Error::missing_field("state"))?; + let attributes = attributes.ok_or_else(|| de::Error::missing_field("attributes"))?; + + let Some((kind, _)) = entity_id.split_once('.') else { + return Err(de::Error::custom("invalid entity_id")); + }; + + let attributes = match kind { + "sun" => StateAttributes::Light(serde_json::from_str(attributes.get()).unwrap()), + "media_player" => { + StateAttributes::MediaPlayer(serde_json::from_str(attributes.get()).unwrap()) + } + "camera" => { + StateAttributes::Camera(serde_json::from_str(attributes.get()).unwrap()) + } + "weather" => { + StateAttributes::Weather(serde_json::from_str(attributes.get()).unwrap()) + } + "light" => StateAttributes::Light(serde_json::from_str(attributes.get()).unwrap()), + _ => StateAttributes::Unknown, + }; + + Ok(State { + entity_id, + state, + attributes, + }) } } #[derive(Deserialize, Debug)] + pub enum StateAttributes<'a> { + Sun(StateSunAttributes), + MediaPlayer(#[serde(borrow)] StateMediaPlayerAttributes<'a>), + Camera(#[serde(borrow)] StateCameraAttributes<'a>), + Weather(#[serde(borrow)] StateWeatherAttributes<'a>), + Light(#[serde(borrow)] StateLightAttributes<'a>), + Unknown, + } + + #[derive(Deserialize, Debug)] pub struct StateSunAttributes { // next_dawn: time::OffsetDateTime, // next_dusk: time::OffsetDateTime, @@ -319,27 +392,27 @@ pub mod responses { #[derive(Deserialize, Debug)] pub struct StateMediaPlayerAttributes<'a> { - #[serde(borrow)] + #[serde(borrow, default)] source_list: Vec>, - #[serde(borrow)] + #[serde(borrow, default)] group_members: Vec>, - volume_level: f32, - is_volume_muted: bool, + volume_level: Option, + is_volume_muted: Option, #[serde(borrow)] - media_content_id: Cow<'a, str>, + media_content_id: Option>, #[serde(borrow)] - media_content_type: Cow<'a, str>, + media_content_type: Option>, #[serde(borrow)] - source: Cow<'a, str>, - shuffle: bool, + source: Option>, + shuffle: Option, #[serde(borrow)] - repeat: Cow<'a, str>, - queue_position: u32, - queue_size: u32, + repeat: Option>, + queue_position: Option, + queue_size: Option, #[serde(borrow)] - device_class: Cow<'a, str>, + device_class: Option>, #[serde(borrow)] - friendly_name: Cow<'a, str>, + friendly_name: Option>, } #[derive(Deserialize, Debug)] @@ -349,72 +422,142 @@ pub mod responses { #[serde(borrow)] friendly_name: Cow<'a, str>, #[serde(borrow)] - stream_source: Cow<'a, str>, + stream_source: Option>, #[serde(borrow)] - still_image_url: Cow<'a, str>, + still_image_url: Option>, #[serde(borrow)] - name: Cow<'a, str>, + name: Option>, #[serde(borrow)] - id: Cow<'a, str>, + id: Option>, #[serde(borrow)] entity_picture: Cow<'a, str>, } + #[derive(Deserialize, Debug, EnumString, Copy, Clone)] + #[serde(rename_all = "kebab-case")] + #[strum(serialize_all = "kebab-case")] + pub enum WeatherCondition { + ClearNight, + Cloudy, + Fog, + Hail, + Lightning, + LightningRainy, + #[serde(rename = "partlycloudy")] + #[strum(serialize = "partlycloudy")] + PartlyCloudy, + Pouring, + Rainy, + Snowy, + SnowyRainy, + Sunny, + Windy, + WindyVariant, + Exceptional, + #[serde(other)] + Unknown, + } + + impl WeatherCondition { + pub fn icon(self, day_time: bool) -> Option { + match self { + WeatherCondition::ClearNight => Some(Icon::ClearNight), + WeatherCondition::Cloudy => Some(Icon::Cloud), + WeatherCondition::Fog => Some(Icon::Fog), + WeatherCondition::Hail => Some(Icon::Hail), + WeatherCondition::Lightning => Some(Icon::Thunderstorms), + WeatherCondition::LightningRainy => Some(Icon::ThunderstormsRain), + WeatherCondition::PartlyCloudy => Some(if day_time { + Icon::PartlyCloudyDay + } else { + Icon::PartlyCloudyNight + }), + WeatherCondition::Pouring => Some(Icon::ExtremeRain), + WeatherCondition::Rainy => Some(Icon::Rain), + WeatherCondition::Snowy | WeatherCondition::SnowyRainy => Some(Icon::Snow), + WeatherCondition::Sunny => Some(Icon::ClearDay), + WeatherCondition::Windy | WeatherCondition::WindyVariant => Some(Icon::Wind), + WeatherCondition::Exceptional | WeatherCondition::Unknown => None, + } + } + } + + impl Display for WeatherCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + WeatherCondition::ClearNight => "Clear", + WeatherCondition::Cloudy => "Cloudy", + WeatherCondition::Fog => "Fog", + WeatherCondition::Hail => "Hail", + WeatherCondition::Lightning | WeatherCondition::LightningRainy => "Lightning", + WeatherCondition::PartlyCloudy => "Partly Cloudy", + WeatherCondition::Pouring => "Heavy Rain", + WeatherCondition::Rainy => "Rain", + WeatherCondition::Snowy | WeatherCondition::SnowyRainy => "Snow", + WeatherCondition::Sunny => "Sunny", + WeatherCondition::Windy | WeatherCondition::WindyVariant => "Windy", + WeatherCondition::Exceptional => "Exceptional", + WeatherCondition::Unknown => "Unknown", + }) + } + } + #[derive(Deserialize, Debug)] pub struct StateWeatherAttributes<'a> { - temperature: f32, - dew_point: f32, + pub temperature: f32, + pub dew_point: f32, #[serde(borrow)] - temperature_unit: Cow<'a, str>, - humidity: u8, - cloud_coverage: u8, - pressure: f32, + pub temperature_unit: Cow<'a, str>, + pub humidity: f32, + pub cloud_coverage: f32, + pub pressure: f32, #[serde(borrow)] - pressure_unit: Cow<'a, str>, - wind_bearing: f32, - wind_speed: f32, + pub pressure_unit: Cow<'a, str>, + pub wind_bearing: f32, + pub wind_speed: f32, #[serde(borrow)] - wind_speed_unit: Cow<'a, str>, + pub wind_speed_unit: Cow<'a, str>, #[serde(borrow)] - visibility_unit: Cow<'a, str>, + pub visibility_unit: Cow<'a, str>, #[serde(borrow)] - precipitation_unit: Cow<'a, str>, + pub precipitation_unit: Cow<'a, str>, #[serde(borrow)] - forecast: Vec>, + pub forecast: Vec>, } #[derive(Deserialize, Debug)] pub struct StateWeatherAttributesForecast<'a> { #[serde(borrow)] - condition: Cow<'a, str>, + pub condition: Cow<'a, str>, // datetime: time::OffsetDateTime, - wind_bearing: f32, - temperature: f32, + pub wind_bearing: f32, + pub temperature: f32, #[serde(rename = "templow")] - temperature_low: f32, - wind_speed: f32, - precipitation: u8, - humidity: u8, + pub temperature_low: f32, + pub wind_speed: f32, + pub precipitation: f32, + pub humidity: f32, } #[derive(Deserialize, Debug)] pub struct StateLightAttributes<'a> { - min_color_temp_kelvin: u16, - max_color_temp_kelvin: u16, - min_mireds: u16, - max_mireds: u16, + min_color_temp_kelvin: Option, + max_color_temp_kelvin: Option, + min_mireds: Option, + max_mireds: Option, + #[serde(default)] supported_color_modes: Vec, #[serde(borrow)] - mode: Cow<'a, str>, + mode: Option>, #[serde(borrow)] - dynamics: Cow<'a, str>, + dynamics: Option>, #[serde(borrow)] friendly_name: Cow<'a, str>, color_mode: Option, - brightness: Option, + brightness: Option, color_temp_kelvin: Option, color_temp: Option, - xy_color: Option<(u8, u8)>, + xy_color: Option<(f32, f32)>, } #[derive(Deserialize, Debug)] diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs index 3a0c878..d1acf95 100644 --- a/shalom/src/oracle.rs +++ b/shalom/src/oracle.rs @@ -1,21 +1,28 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, str::FromStr}; use internment::Intern; -use crate::hass_client::{responses::AreaRegistryList, HassRequestKind}; +use crate::hass_client::{ + responses::{AreaRegistryList, StateAttributes, StatesList, WeatherCondition}, + HassRequestKind, +}; #[derive(Debug)] pub struct Oracle { client: crate::hass_client::Client, rooms: BTreeMap, Room>, + pub weather: Weather, } impl Oracle { pub async fn new(hass_client: crate::hass_client::Client) -> Self { - let (rooms,) = tokio::join!( - hass_client.request::>(HassRequestKind::AreaRegistry) + let (rooms, states) = tokio::join!( + hass_client.request::>(HassRequestKind::AreaRegistry), + hass_client.request::>(HassRequestKind::GetStates), ); + let states = states.get(); + let rooms = rooms .get() .0 @@ -33,6 +40,7 @@ impl Oracle { Self { client: hass_client, rooms, + weather: Weather::parse_from_states(states), } } @@ -45,3 +53,44 @@ impl Oracle { pub struct Room { pub name: Intern, } + +#[derive(Debug)] +pub struct Weather { + pub temperature: i16, + pub high: i16, + pub low: i16, + pub condition: WeatherCondition, +} + +impl Weather { + fn parse_from_states(states: &StatesList) -> Self { + let (state, weather) = states + .0 + .iter() + .filter_map(|v| match &v.attributes { + StateAttributes::Weather(attr) => Some((&v.state, attr)), + _ => None, + }) + .next() + .unwrap(); + + let condition = WeatherCondition::from_str(&state).unwrap_or(WeatherCondition::Unknown); + + let (high, low) = + weather + .forecast + .iter() + .fold((i16::MIN, i16::MAX), |(high, low), curr| { + let temp = curr.temperature.round() as i16; + + (high.max(temp), low.min(temp)) + }); + + Self { + temperature: weather.temperature.round() as i16, + condition, + high, + low, + } + } +} diff --git a/shalom/src/pages/omni.rs b/shalom/src/pages/omni.rs index 0ea17be..ed9b8fc 100644 --- a/shalom/src/pages/omni.rs +++ b/shalom/src/pages/omni.rs @@ -32,13 +32,11 @@ impl Component for Omni { } fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> { - let header = |v| { - text(v).size(60).font(Font { - weight: Weight::Bold, - stretch: Stretch::Condensed, - ..Font::with_name("Helvetica Neue") - }) - }; + let greeting = text("Good Evening").size(60).font(Font { + weight: Weight::Bold, + stretch: Stretch::Condensed, + ..Font::with_name("Helvetica Neue") + }); let room = |room, image| { image_card::image_card(image, room).on_press(Event::OpenRoom(room)) @@ -56,9 +54,13 @@ impl Component for Omni { .fold(Column::new().spacing(10), Column::push); scrollable( - column![header("Cameras"), header("Rooms"), rooms,] - .spacing(20) - .padding(40), + column![ + greeting, + crate::widgets::cards::weather::WeatherCard::new(self.oracle.clone()), + rooms, + ] + .spacing(20) + .padding(40), ) .into() } diff --git a/shalom/src/theme.rs b/shalom/src/theme.rs index 3f57e9c..77466fc 100644 --- a/shalom/src/theme.rs +++ b/shalom/src/theme.rs @@ -43,30 +43,56 @@ pub enum Icon { Play, Pause, Repeat, + Cloud, + ClearNight, + Fog, + Hail, + Thunderstorms, + ThunderstormsRain, + PartlyCloudyDay, + PartlyCloudyNight, + ExtremeRain, + Rain, + Snow, + ClearDay, + Wind, } impl Icon { pub fn handle(self) -> svg::Handle { macro_rules! image { ($path:expr) => {{ - static FILE: &[u8] = include_bytes!($path); + static FILE: &[u8] = include_bytes!(concat!("../../assets/icons/", $path, ".svg")); static HANDLE: Lazy = Lazy::new(|| svg::Handle::from_memory(FILE)); (*HANDLE).clone() }}; } match self { - Self::Home => image!("../../assets/icons/home.svg"), - Self::Back => image!("../../assets/icons/back.svg"), - Self::Bulb => image!("../../assets/icons/light-bulb.svg"), - Self::Hamburger => image!("../../assets/icons/hamburger.svg"), - Self::Speaker => image!("../../assets/icons/speaker.svg"), - Self::SpeakerMuted => image!("../../assets/icons/speaker-muted.svg"), - Self::Backward => image!("../../assets/icons/backward.svg"), - Self::Forward => image!("../../assets/icons/forward.svg"), - Self::Play => image!("../../assets/icons/play.svg"), - Self::Pause => image!("../../assets/icons/pause.svg"), - Self::Repeat => image!("../../assets/icons/repeat.svg"), + Self::Home => image!("home"), + Self::Back => image!("back"), + Self::Bulb => image!("light-bulb"), + Self::Hamburger => image!("hamburger"), + Self::Speaker => image!("speaker"), + Self::SpeakerMuted => image!("speaker-muted"), + Self::Backward => image!("backward"), + Self::Forward => image!("forward"), + Self::Play => image!("play"), + Self::Pause => image!("pause"), + Self::Repeat => image!("repeat"), + Self::Cloud => image!("cloud"), + Self::ClearNight => image!("clear-night"), + Self::Fog => image!("fog"), + Self::Hail => image!("hail"), + Self::Thunderstorms => image!("thunderstorms"), + Self::ThunderstormsRain => image!("thunderstorms-rain"), + Self::PartlyCloudyDay => image!("partly-cloudy-day"), + Self::PartlyCloudyNight => image!("partly-cloudy-night"), + Self::ExtremeRain => image!("extreme-rain"), + Self::Rain => image!("rain"), + Self::Snow => image!("snow"), + Self::ClearDay => image!("clear-day"), + Self::Wind => image!("wind"), } } } diff --git a/shalom/src/widgets/cards/mod.rs b/shalom/src/widgets/cards/mod.rs new file mode 100644 index 0000000..ef98bcf --- /dev/null +++ b/shalom/src/widgets/cards/mod.rs @@ -0,0 +1 @@ +pub mod weather; diff --git a/shalom/src/widgets/cards/weather.rs b/shalom/src/widgets/cards/weather.rs new file mode 100644 index 0000000..473f4eb --- /dev/null +++ b/shalom/src/widgets/cards/weather.rs @@ -0,0 +1,173 @@ +use std::sync::Arc; + +use iced::{ + advanced::{ + layout::{Limits, Node}, + renderer::{Quad, Style}, + svg::Renderer as SvgRenderer, + text::{LineHeight, Renderer as TextRenderer, Shaping}, + widget::Tree, + Layout, Renderer as AdvancedRenderer, Text, Widget, + }, + alignment::{Horizontal, Vertical}, + font::Weight, + gradient::Linear, + mouse::Cursor, + Alignment, Background, Color, Degrees, Element, Font, Gradient, Length, Rectangle, Renderer, + Size, Theme, +}; + +use crate::oracle::Oracle; + +pub struct WeatherCard { + pub on_click: Option, + pub oracle: Arc, +} + +impl WeatherCard { + pub fn new(oracle: Arc) -> Self { + Self { + on_click: None, + oracle, + } + } + + fn build_temperature(&self) -> String { + format!("{}°", self.oracle.weather.temperature) + } + + fn build_conditions(&self) -> String { + format!( + "{}\nH:{}° L:{}°", + self.oracle.weather.condition, self.oracle.weather.high, self.oracle.weather.low, + ) + } +} + +impl Widget for WeatherCard { + fn width(&self) -> Length { + Length::Fixed(192.) + } + + fn height(&self) -> Length { + Length::Fixed(192.) + } + + fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + let padding = 16.into(); + + let limits = limits + .height(self.height()) + .width(self.width()) + .pad(padding); + let container_size = limits.resolve(Size::ZERO); + + let mut header_node = Node::new(renderer.measure( + &self.build_temperature(), + 42., + LineHeight::default(), + Font { + weight: Weight::Normal, + ..Font::with_name("Helvetica Neue") + }, + container_size, + Shaping::Basic, + )); + header_node.move_to([padding.top, padding.left].into()); + header_node.align(Alignment::Start, Alignment::Start, container_size); + + let mut icon_node = + Node::new(Size::new(16., 16.)).translate([padding.left, -padding.bottom - 32.].into()); + icon_node.align(Alignment::Start, Alignment::End, container_size); + + let mut conditions_node = Node::new(renderer.measure( + &self.build_conditions(), + 12., + LineHeight::default(), + Font { + weight: Weight::Bold, + ..Font::with_name("Helvetica Neue") + }, + container_size, + Shaping::Basic, + )) + .translate([padding.left, -padding.bottom].into()); + conditions_node.align(Alignment::Start, Alignment::End, container_size); + + Node::with_children( + container_size, + vec![header_node, icon_node, conditions_node], + ) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &Style, + layout: Layout<'_>, + _cursor: Cursor, + _viewport: &Rectangle, + ) { + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border_radius: [20., 20., 20., 20.].into(), + border_width: 0., + border_color: Color::WHITE, + }, + Background::Gradient(Gradient::Linear( + Linear::new(Degrees(90.)) + .add_stop(0.0, Color::from_rgba8(43, 44, 66, 1.0)) + .add_stop(1.0, Color::from_rgba8(15, 18, 27, 1.0)), + )), + ); + + let mut children = layout.children(); + + renderer.fill_text(Text { + content: &self.build_temperature(), + bounds: children.next().unwrap().bounds(), + size: 42., + line_height: LineHeight::default(), + color: Color::WHITE, + font: Font { + weight: Weight::Normal, + ..Font::with_name("Helvetica Neue") + }, + horizontal_alignment: Horizontal::Left, + vertical_alignment: Vertical::Top, + shaping: Shaping::Basic, + }); + + let icon_bounds = children.next().unwrap().bounds(); + if let Some(icon) = self.oracle.weather.condition.icon(false) { + renderer.draw(icon.handle(), None, icon_bounds); + } + + renderer.fill_text(Text { + content: &self.build_conditions(), + bounds: children.next().unwrap().bounds(), + size: 12., + line_height: LineHeight::default(), + color: Color::WHITE, + font: Font { + weight: Weight::Bold, + ..Font::with_name("Helvetica Neue") + }, + horizontal_alignment: Horizontal::Left, + vertical_alignment: Vertical::Top, + shaping: Shaping::Basic, + }); + } +} + +impl<'a, M> From> for Element<'a, M> +where + M: 'a + Clone, +{ + fn from(modal: WeatherCard) -> Self { + Element::new(modal) + } +} diff --git a/shalom/src/widgets/mod.rs b/shalom/src/widgets/mod.rs index 413cbd5..e2450e3 100644 --- a/shalom/src/widgets/mod.rs +++ b/shalom/src/widgets/mod.rs @@ -1,3 +1,4 @@ +pub mod cards; pub mod context_menu; pub mod image_card; pub mod media_player; -- libgit2 1.7.2