This commit is contained in:
parent
265f8d8dc3
commit
450183c06b
5 changed files with 419 additions and 70 deletions
|
@ -1,30 +1,35 @@
|
|||
use std::{
|
||||
cell::OnceCell,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{self, Receiver, Sender},
|
||||
},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use openweathermap_lib::{
|
||||
location::Location,
|
||||
weather::{WeatherClient, WeatherResponse},
|
||||
};
|
||||
use rand::{rngs::OsRng, seq::IndexedRandom, TryRngCore};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use ratzilla::ratatui;
|
||||
|
||||
use dotenvy_macro::dotenv;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
widgets::{Block, Borders, Paragraph, Tabs},
|
||||
Frame,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tachyonfx::{fx, Effect, Interpolation, Shader};
|
||||
use web_time::Instant;
|
||||
|
||||
use crate::app::password_locker::PasswordLocker;
|
||||
use crate::app::{
|
||||
password_locker::PasswordLocker,
|
||||
weather_icon::{WeatherIcon, DAY_ICONS},
|
||||
};
|
||||
|
||||
mod password_locker;
|
||||
mod weather_icon;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct SelectedTab {
|
||||
|
@ -68,32 +73,11 @@ impl Shader for SelectedTab {
|
|||
}
|
||||
|
||||
pub trait AppExecutor {
|
||||
fn execute<T: 'static>(&self, future: impl Future<Output = T> + 'static, sender: Sender<T>);
|
||||
}
|
||||
|
||||
pub struct Pending<T> {
|
||||
rx: Receiver<T>,
|
||||
resolved: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Pending<T> {
|
||||
pub fn new(rx: Receiver<T>) -> Self {
|
||||
Self { rx, resolved: None }
|
||||
}
|
||||
|
||||
pub fn resolved(&mut self) -> &Option<T> {
|
||||
if self.resolved.is_some() {
|
||||
return &self.resolved;
|
||||
}
|
||||
|
||||
self.resolved = self.rx.try_recv().ok();
|
||||
&self.resolved
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct WeatherInfo {
|
||||
name: String,
|
||||
fn execute<T: 'static>(
|
||||
&self,
|
||||
future: impl Future<Output = T> + 'static,
|
||||
output_cell: Rc<OnceCell<T>>,
|
||||
);
|
||||
}
|
||||
|
||||
pub struct App<E: AppExecutor> {
|
||||
|
@ -102,7 +86,7 @@ pub struct App<E: AppExecutor> {
|
|||
selected_tab: SelectedTab,
|
||||
password_locker: PasswordLocker,
|
||||
current_effect: Effect,
|
||||
weather: Pending<WeatherInfo>,
|
||||
weather: Rc<OnceCell<WeatherResponse>>,
|
||||
executor: E,
|
||||
}
|
||||
|
||||
|
@ -148,24 +132,31 @@ const TABS: [&'static str; 10] = [
|
|||
|
||||
impl<E: AppExecutor> App<E> {
|
||||
pub fn new(executor: E) -> Self {
|
||||
let (weather_tx, weather_rx) = mpsc::channel();
|
||||
executor.execute(async move {
|
||||
let weather_info: WeatherInfo = reqwest::get(concat!("https://api.openweathermap.org/data/2.5/weather?lat=40.7660851712019&lon=-111.89066476757807&appid=", dotenv!("OPENWEATHERMAP_API_KEY")))
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
weather_info
|
||||
},
|
||||
weather_tx);
|
||||
let weather = Rc::new(OnceCell::new());
|
||||
executor.execute(
|
||||
async move {
|
||||
let weather_client = WeatherClient::new(
|
||||
Location {
|
||||
zip: String::from("84111"),
|
||||
name: String::from("Salt Lake City"),
|
||||
lat: 40.7660851712019,
|
||||
lon: -111.89066476757807,
|
||||
country: String::from("US"),
|
||||
},
|
||||
"metric".to_owned(),
|
||||
dotenv!("OPENWEATHERMAP_API_KEY").to_owned(),
|
||||
);
|
||||
weather_client.get_current_weather().await.unwrap()
|
||||
},
|
||||
Rc::clone(&weather),
|
||||
);
|
||||
Self {
|
||||
tabs: TABS.choose_multiple_array(&mut OsRng.unwrap_err()).unwrap(),
|
||||
transition_instant: Instant::now(),
|
||||
selected_tab: SelectedTab::default(),
|
||||
password_locker: PasswordLocker::default(),
|
||||
current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
|
||||
weather: Pending::new(weather_rx),
|
||||
weather,
|
||||
executor,
|
||||
}
|
||||
}
|
||||
|
@ -211,26 +202,85 @@ impl<E: AppExecutor> App<E> {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn draw_first_tab(&mut self, frame: &mut Frame, layout: Rect) {
|
||||
pub fn draw_first_tab(&mut self, frame: &mut Frame, layout_outer: Rect) {
|
||||
let layout = Layout::new(
|
||||
Direction::Horizontal,
|
||||
if layout_outer.width < (layout_outer.height * 2) {
|
||||
Direction::Vertical
|
||||
} else {
|
||||
Direction::Horizontal
|
||||
},
|
||||
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||
)
|
||||
.split(layout);
|
||||
frame.render_widget(
|
||||
Paragraph::new(
|
||||
self.weather
|
||||
.resolved()
|
||||
.as_ref()
|
||||
.map(|weather| weather.name.as_str())
|
||||
.unwrap_or("Loading"),
|
||||
.split(layout_outer);
|
||||
if let Some(weather) = self.weather.get() {
|
||||
frame.render_widget(
|
||||
Block::new()
|
||||
.title("Weather")
|
||||
.borders(Borders::all())
|
||||
.fg(Color::Rgb(226, 190, 89))
|
||||
.bg(Color::Black),
|
||||
layout[0],
|
||||
);
|
||||
|
||||
let layout = Layout::new(
|
||||
Direction::Vertical,
|
||||
[Constraint::Length(12), Constraint::Min(0)],
|
||||
)
|
||||
.alignment(ratatui::layout::Alignment::Center)
|
||||
.fg(Color::Rgb(226, 190, 89))
|
||||
.bg(Color::Black)
|
||||
.block(Block::new().title("Weather").borders(Borders::all())),
|
||||
layout[0],
|
||||
);
|
||||
.split(layout[0].inner(Margin::new(2, 2)));
|
||||
|
||||
let layout_upper = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||
)
|
||||
.split(layout[0]);
|
||||
|
||||
frame.render_widget(
|
||||
WeatherIcon(DAY_ICONS.get(&weather.weather[0].icon).unwrap()),
|
||||
layout_upper[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!(
|
||||
"{} {}°C\n~{}°C -{}°C +{}°C\n{} {} {}m/s SSE\nVisibility: {}km",
|
||||
weather.name,
|
||||
weather.main.temp.unwrap_or(f64::NAN),
|
||||
weather.main.feels_like.unwrap_or(f64::NAN),
|
||||
weather.main.temp_min.unwrap_or(f64::NAN),
|
||||
weather.main.temp_max.unwrap_or(f64::NAN),
|
||||
weather.weather[0].main,
|
||||
match weather.wind.deg {
|
||||
0..=45 => "🡓",
|
||||
46..=90 => "🡗",
|
||||
91..=135 => "🡐",
|
||||
136..=180 => "🡔",
|
||||
181..=225 => "🡑",
|
||||
226..=270 => "🡕",
|
||||
271..=315 => "🡒",
|
||||
_ => "🡖",
|
||||
},
|
||||
weather.wind.speed,
|
||||
weather.visibility as f64 / 1000.0,
|
||||
))
|
||||
.fg(Color::Rgb(226, 190, 89))
|
||||
.bg(Color::Black),
|
||||
layout_upper[1],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::new()
|
||||
.borders(Borders::all())
|
||||
.fg(Color::Rgb(226, 190, 89))
|
||||
.bg(Color::Black),
|
||||
layout[1],
|
||||
);
|
||||
} else {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Loading")
|
||||
.alignment(ratatui::layout::Alignment::Center)
|
||||
.fg(Color::Rgb(226, 190, 89))
|
||||
.bg(Color::Black)
|
||||
.block(Block::new().title("Weather").borders(Borders::all())),
|
||||
layout[0],
|
||||
);
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(SPLASH)
|
||||
.alignment(ratatui::layout::Alignment::Center)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue