Add forecast support
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
f719983c62
commit
2c2246ae00
4 changed files with 90 additions and 41 deletions
|
@ -37,13 +37,17 @@ wasm-bindgen-test = "0.3.34"
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# Tell `rustc` to optimize for small code size.
|
# Tell `rustc` to optimize for small code size.
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dotenvy_macro = "0.15.7"
|
dotenvy_macro = "0.15.7"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
getrandom = { version = "0.3.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3.3", features = ["wasm_js"] }
|
||||||
heapless = "0.8.0"
|
heapless = "0.8.0"
|
||||||
openweathermap_lib = { git = "https://github.com/StratusFearMe21/rusty-openweathermap-library", branch = "wasm_deps" }
|
openweathermap_lib = { git = "https://github.com/StratusFearMe21/rusty-openweathermap-library", branch = "forecast" }
|
||||||
phf = { version = "0.12.1", features = ["macros"] }
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] }
|
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] }
|
||||||
tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] }
|
tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] }
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use openweathermap_lib::{
|
use openweathermap_lib::{
|
||||||
|
forecast::{ForecastClient, ForecastResponse},
|
||||||
location::Location,
|
location::Location,
|
||||||
weather::{WeatherClient, WeatherResponse},
|
weather::{WeatherClient, WeatherResponse},
|
||||||
};
|
};
|
||||||
|
@ -17,7 +18,7 @@ use dotenvy_macro::dotenv;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
widgets::{Block, Borders, Paragraph, Tabs},
|
widgets::{Block, Borders, List, ListState, Paragraph, Tabs},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use tachyonfx::{fx, Effect, Interpolation, Shader};
|
use tachyonfx::{fx, Effect, Interpolation, Shader};
|
||||||
|
@ -25,7 +26,7 @@ use web_time::Instant;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
password_locker::PasswordLocker,
|
password_locker::PasswordLocker,
|
||||||
weather_icon::{WeatherIcon, WEATHER_ICONS},
|
weather_icon::{WeatherIcon, WEATHER_ICONS, WEATHER_ICONS_SMALL},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod password_locker;
|
mod password_locker;
|
||||||
|
@ -86,7 +87,8 @@ pub struct App<E: AppExecutor> {
|
||||||
selected_tab: SelectedTab,
|
selected_tab: SelectedTab,
|
||||||
password_locker: PasswordLocker,
|
password_locker: PasswordLocker,
|
||||||
current_effect: Effect,
|
current_effect: Effect,
|
||||||
weather: Rc<OnceCell<WeatherResponse>>,
|
forecast_state: ListState,
|
||||||
|
weather: Rc<OnceCell<(WeatherResponse, ForecastResponse)>>,
|
||||||
executor: E,
|
executor: E,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,18 +137,24 @@ impl<E: AppExecutor> App<E> {
|
||||||
let weather = Rc::new(OnceCell::new());
|
let weather = Rc::new(OnceCell::new());
|
||||||
executor.execute(
|
executor.execute(
|
||||||
async move {
|
async move {
|
||||||
let weather_client = WeatherClient::new(
|
let location = Location {
|
||||||
Location {
|
|
||||||
zip: String::from("84111"),
|
zip: String::from("84111"),
|
||||||
name: String::from("Salt Lake City"),
|
name: String::from("Salt Lake City"),
|
||||||
lat: 40.7660851712019,
|
lat: 40.7660851712019,
|
||||||
lon: -111.89066476757807,
|
lon: -111.89066476757807,
|
||||||
country: String::from("US"),
|
country: String::from("US"),
|
||||||
},
|
};
|
||||||
"imperial".to_owned(),
|
let units = "imperial".to_owned();
|
||||||
dotenv!("OPENWEATHERMAP_API_KEY").to_owned(),
|
let api_key = dotenv!("OPENWEATHERMAP_API_KEY").to_owned();
|
||||||
);
|
let weather_client =
|
||||||
weather_client.get_current_weather().await.unwrap()
|
WeatherClient::new(location.clone(), units.clone(), api_key.clone());
|
||||||
|
let forecast_client = ForecastClient::new(location, units, api_key, None);
|
||||||
|
futures_util::future::try_join(
|
||||||
|
weather_client.get_current_weather(),
|
||||||
|
forecast_client.get_current_forecast(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
},
|
},
|
||||||
Rc::clone(&weather),
|
Rc::clone(&weather),
|
||||||
);
|
);
|
||||||
|
@ -156,6 +164,7 @@ impl<E: AppExecutor> App<E> {
|
||||||
selected_tab: SelectedTab::default(),
|
selected_tab: SelectedTab::default(),
|
||||||
password_locker: PasswordLocker::default(),
|
password_locker: PasswordLocker::default(),
|
||||||
current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
|
current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
|
||||||
|
forecast_state: ListState::default(),
|
||||||
weather,
|
weather,
|
||||||
executor,
|
executor,
|
||||||
}
|
}
|
||||||
|
@ -225,33 +234,22 @@ impl<E: AppExecutor> App<E> {
|
||||||
|
|
||||||
let layout = Layout::new(
|
let layout = Layout::new(
|
||||||
Direction::Vertical,
|
Direction::Vertical,
|
||||||
[Constraint::Length(20), Constraint::Min(0)],
|
[Constraint::Length(12), Constraint::Min(0)],
|
||||||
)
|
)
|
||||||
.split(weather_area.inner(Margin::new(2, 2)));
|
.split(weather_area.inner(Margin::new(2, 2)));
|
||||||
|
|
||||||
let layout_upper = Layout::new(
|
let layout_upper = Layout::new(
|
||||||
if weather_area.width < (weather_area.height * 2) {
|
Direction::Horizontal,
|
||||||
Direction::Vertical
|
[Constraint::Length(32), Constraint::Min(0)],
|
||||||
} else {
|
|
||||||
Direction::Horizontal
|
|
||||||
},
|
|
||||||
[
|
|
||||||
Constraint::Length(if weather_area.width < (weather_area.height * 2) {
|
|
||||||
28 / 2
|
|
||||||
} else {
|
|
||||||
28
|
|
||||||
}),
|
|
||||||
Constraint::Min(0),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.split(layout[0]);
|
.split(layout[0]);
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
WeatherIcon(
|
WeatherIcon(
|
||||||
WEATHER_ICONS
|
WEATHER_ICONS
|
||||||
.get(&weather.weather[0].icon[..weather.weather[0].icon.len() - 1])
|
.get(&weather.0.weather[0].icon[..weather.0.weather[0].icon.len() - 1])
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
weather.weather[0].icon[weather.weather[0].icon.len() - 1..]
|
weather.0.weather[0].icon[weather.0.weather[0].icon.len() - 1..]
|
||||||
.bytes()
|
.bytes()
|
||||||
.next()
|
.next()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
@ -261,13 +259,13 @@ impl<E: AppExecutor> App<E> {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(format!(
|
Paragraph::new(format!(
|
||||||
"{} {}°F\n~{}°F -{}°F +{}°F\n{} {} {}mph\nVisibility: {}km",
|
"{} {}°F\n~{}°F -{}°F +{}°F\n{} {} {}mph\nVisibility: {}km",
|
||||||
weather.name,
|
weather.0.name,
|
||||||
weather.main.temp.unwrap_or(f64::NAN),
|
weather.0.main.temp.unwrap_or(f64::NAN),
|
||||||
weather.main.feels_like.unwrap_or(f64::NAN),
|
weather.0.main.feels_like.unwrap_or(f64::NAN),
|
||||||
weather.main.temp_min.unwrap_or(f64::NAN),
|
weather.0.main.temp_min.unwrap_or(f64::NAN),
|
||||||
weather.main.temp_max.unwrap_or(f64::NAN),
|
weather.0.main.temp_max.unwrap_or(f64::NAN),
|
||||||
weather.weather[0].main,
|
weather.0.weather[0].main,
|
||||||
match weather.wind.deg {
|
match weather.0.wind.deg {
|
||||||
0..=45 => "🡓",
|
0..=45 => "🡓",
|
||||||
46..=90 => "🡗",
|
46..=90 => "🡗",
|
||||||
91..=135 => "🡐",
|
91..=135 => "🡐",
|
||||||
|
@ -277,13 +275,32 @@ impl<E: AppExecutor> App<E> {
|
||||||
271..=315 => "🡒",
|
271..=315 => "🡒",
|
||||||
_ => "🡖",
|
_ => "🡖",
|
||||||
},
|
},
|
||||||
weather.wind.speed,
|
weather.0.wind.speed,
|
||||||
weather.visibility as f64 / 1000.0,
|
weather.0.visibility as f64 / 1000.0,
|
||||||
))
|
))
|
||||||
.fg(Color::Rgb(226, 190, 89))
|
.fg(Color::Rgb(226, 190, 89))
|
||||||
.bg(Color::Black),
|
.bg(Color::Black),
|
||||||
layout_upper[1],
|
layout_upper[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
frame.render_stateful_widget(
|
||||||
|
List::new(weather.1.list.iter().map(|item| {
|
||||||
|
format!(
|
||||||
|
"{} {:<20} {:<6}°F ~{:<6}°F {}",
|
||||||
|
WEATHER_ICONS_SMALL
|
||||||
|
.get(&item.weather[0].icon[..item.weather[0].icon.len() - 1])
|
||||||
|
.unwrap(),
|
||||||
|
item.weather[0].description.as_str(),
|
||||||
|
item.main.temp.unwrap_or(f64::NAN),
|
||||||
|
item.main.temp_max.unwrap_or(f64::NAN),
|
||||||
|
item.dt_txt
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.block(Block::new().title("5-day forecast").borders(Borders::all()))
|
||||||
|
.highlight_symbol("> "),
|
||||||
|
layout[1],
|
||||||
|
&mut self.forecast_state,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new("Loading")
|
Paragraph::new("Loading")
|
||||||
|
@ -346,6 +363,14 @@ impl<E: AppExecutor> App<E> {
|
||||||
self.tab_transition();
|
self.tab_transition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn next_item(&mut self) {
|
||||||
|
self.forecast_state.select_next();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_item(&mut self) {
|
||||||
|
self.forecast_state.select_previous();
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_transition(&mut self) {
|
fn tab_transition(&mut self) {
|
||||||
self.transition_instant = Instant::now();
|
self.transition_instant = Instant::now();
|
||||||
self.current_effect = fx::sequence(&[
|
self.current_effect = fx::sequence(&[
|
||||||
|
|
|
@ -267,6 +267,18 @@ pub const WEATHER_ICONS: phf::Map<&'static str, [&'static str; 3]> = phf_map! {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const WEATHER_ICONS_SMALL: phf::Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"01" => "☀️",
|
||||||
|
"02" => "⛅",
|
||||||
|
"03" => "🌥️",
|
||||||
|
"04" => "☁️",
|
||||||
|
"09" => "🌧️",
|
||||||
|
"10" => "🌦️",
|
||||||
|
"11" => "🌩️",
|
||||||
|
"13" => "❄️",
|
||||||
|
"50" => "🌫️",
|
||||||
|
};
|
||||||
|
|
||||||
pub struct WeatherIcon(pub &'static [&'static str; 3], pub u8);
|
pub struct WeatherIcon(pub &'static [&'static str; 3], pub u8);
|
||||||
|
|
||||||
impl Widget for WeatherIcon {
|
impl Widget for WeatherIcon {
|
||||||
|
|
|
@ -45,6 +45,14 @@ fn main() {
|
||||||
code: event::KeyCode::Right,
|
code: event::KeyCode::Right,
|
||||||
..
|
..
|
||||||
}) => app.next_tab(),
|
}) => app.next_tab(),
|
||||||
|
event::Event::Key(event::KeyEvent {
|
||||||
|
code: event::KeyCode::Up,
|
||||||
|
..
|
||||||
|
}) => app.prev_item(),
|
||||||
|
event::Event::Key(event::KeyEvent {
|
||||||
|
code: event::KeyCode::Down,
|
||||||
|
..
|
||||||
|
}) => app.next_item(),
|
||||||
event::Event::Key(event::KeyEvent {
|
event::Event::Key(event::KeyEvent {
|
||||||
code: event::KeyCode::Char(c),
|
code: event::KeyCode::Char(c),
|
||||||
..
|
..
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue