diff --git a/Cargo.lock b/Cargo.lock index 5bef811..6fb534b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "num-traits", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -307,6 +316,7 @@ name = "echoed" version = "0.1.0" dependencies = [ "async-trait", + "chrono", "color-eyre", "cookie_store", "crossterm", @@ -1017,6 +1027,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" diff --git a/echoed/Cargo.toml b/echoed/Cargo.toml index fe63c0a..3baeb4e 100644 --- a/echoed/Cargo.toml +++ b/echoed/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] async-trait = "0.1.83" +chrono = { version = "0.4.38", default-features = false, features = ["now"] } color-eyre = "0.6.3" cookie_store = "0.21.1" crossterm = "0.28.1" diff --git a/echoed/src/prelude.rs b/echoed/src/prelude.rs index f62c454..4e659a4 100644 --- a/echoed/src/prelude.rs +++ b/echoed/src/prelude.rs @@ -5,5 +5,6 @@ pub use color_eyre::Result; pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; pub use ratatui::prelude::*; pub use std::any::Any; +pub use std::borrow::Cow; pub type ViewSender = tokio::sync::mpsc::Sender; diff --git a/echoed/src/views.rs b/echoed/src/views.rs index c355aae..2e343e8 100644 --- a/echoed/src/views.rs +++ b/echoed/src/views.rs @@ -13,14 +13,14 @@ macro_rules! import_view { }; } -import_view!(Auth, Courses); +import_view!(Auth, Courses, Lectures); #[async_trait] -pub trait View { +pub trait View: Send + Sync { async fn setup(&mut self, _state: &State) -> Result<()> { Ok(()) } - fn draw(&self, frame: &mut Frame, state: &State); + fn draw(&mut self, frame: &mut Frame, state: &State); async fn keypress( &mut self, _key: KeyEvent, @@ -31,14 +31,20 @@ pub trait View { } } -pub type ViewContainer = Arc>; +pub type ViewContainer = Arc>; #[macro_export] macro_rules! change_view { - ($tx:expr, $view:ident) => { + (@ $tx:expr, $value:expr) => { $tx.send(Command::ChangeView(std::sync::Arc::new( - tokio::sync::Mutex::new(super::$view::default()), + tokio::sync::Mutex::new($value), ))) .await? }; + ($tx:expr, $view:ident) => { + change_view!(@ $tx, super::$view::default()) + }; + ($tx:expr, $view:expr) => { + change_view!(@ $tx, $view) + }; } diff --git a/echoed/src/views/auth.rs b/echoed/src/views/auth.rs index 2ab2162..851969e 100644 --- a/echoed/src/views/auth.rs +++ b/echoed/src/views/auth.rs @@ -2,7 +2,6 @@ use crate::{helper::echo, prelude::*}; use echoed::DEFAULT_ECHO360; use email_address::EmailAddress; use ratatui::widgets::Paragraph; -use std::borrow::Cow; use tui_textarea::TextArea; const SHOW_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED); @@ -24,7 +23,7 @@ impl<'t> View for Auth<'t> { Ok(()) } - fn draw(&self, frame: &mut Frame, _: &State) { + fn draw(&mut self, frame: &mut Frame, _: &State) { let area = helper::border(frame, Some("Login")); let areas = Layout::vertical([ Constraint::Length(3), diff --git a/echoed/src/views/courses.rs b/echoed/src/views/courses.rs index b601a6c..bf15140 100644 --- a/echoed/src/views/courses.rs +++ b/echoed/src/views/courses.rs @@ -1,8 +1,7 @@ +use crate::{prelude::*, views::Lectures}; use color_eyre::eyre::eyre; use echoed::DEFAULT_ECHO360; - -use crate::prelude::*; -use std::borrow::Cow; +use ratatui::widgets::{List, ListState}; #[derive(Debug)] pub struct Course { @@ -14,6 +13,7 @@ pub struct Course { #[derive(Default)] pub struct Courses { courses: Vec, + selected: ListState, } #[async_trait] @@ -48,23 +48,42 @@ impl View for Courses { .ok_or(eyre!("No courses found"))?; self.courses = enrollments; + self.selected.select_first(); Ok(()) } - fn draw(&self, frame: &mut Frame, _state: &State) { - // let user = state.echo_user.as_ref().unwrap(); - if let Some(x) = self.courses.first() { - frame.render_widget(Line::raw(x.name.to_string()), frame.area()); - } + fn draw(&mut self, frame: &mut Frame, _state: &State) { + let items = self + .courses + .iter() + .map(|course| format!("{} ({})", course.name, course.code)); + let list = List::new(items).highlight_style(Style::new().reversed()); + frame.render_stateful_widget(list, frame.area(), &mut self.selected); } async fn keypress( &mut self, - _key: KeyEvent, + key: KeyEvent, _state: &mut State, - _command_tx: &ViewSender, + command_tx: &ViewSender, ) -> Result<()> { + match key.code { + KeyCode::Up => self.selected.select_previous(), + KeyCode::Down => self.selected.select_next(), + KeyCode::Enter => { + let i = self.selected.selected_mut().unwrap_or(0); + let course = self.courses.get(i).expect("should exist"); + change_view!( + command_tx, + Lectures::new( + format!("{} ({})", course.name, course.code).into(), + course.section_id.clone() + ) + ) + } + _ => {} + } Ok(()) } } diff --git a/echoed/src/views/lectures.rs b/echoed/src/views/lectures.rs new file mode 100644 index 0000000..e460891 --- /dev/null +++ b/echoed/src/views/lectures.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; +use chrono::{DateTime, Utc}; +use color_eyre::eyre::eyre; +use echoed::DEFAULT_ECHO360; +use ratatui::widgets::{List, ListState}; + +pub struct Lecture { + name: Cow<'static, str>, + date: DateTime, +} + +pub struct Lectures { + name: Cow<'static, str>, + section_id: Cow<'static, str>, + lectures: Vec, + count: u16, + selected: ListState, +} + +impl Lectures { + pub fn new(name: Cow<'static, str>, section_id: Cow<'static, str>) -> Self { + Self { + name, + section_id, + lectures: vec![], + count: 0, + selected: ListState::default(), + } + } +} + +#[async_trait] +impl View for Lectures { + async fn setup(&mut self, state: &State) -> Result<()> { + let res = state + .client + .get(format!( + "{}section/{}/syllabus", + DEFAULT_ECHO360.clone(), + self.section_id + )) + .send() + .await? + .json::() + .await?; + + let lectures = res["data"] + .as_array() + .map(|data| { + data.iter() + .map(|data| data["lesson"].clone()) + .map(|lesson| { + lesson["lesson"]["displayName"] + .as_string() + .and_then(|name| { + lesson["endTimeUTC"].as_string().and_then(|date| { + DateTime::parse_from_rfc3339(&date.to_string()).ok().map( + |date| Lecture { + name: name.to_string().into(), + date: date.with_timezone(&Utc), + }, + ) + }) + }) + }) + .collect::>>() + }) + .flatten() + .ok_or(eyre!("No lectures found"))?; + + self.count = lectures.len() as u16; + let now = Utc::now(); + self.lectures = lectures + .into_iter() + .filter(|lecture| lecture.date <= now) + .collect(); + self.selected.select_first(); + + Ok(()) + } + + fn draw(&mut self, frame: &mut Frame, _state: &State) { + let items = self.lectures.iter().map(|lecture| lecture.name.to_string()); + let list = List::new(items).highlight_style(Style::new().reversed()); + frame.render_stateful_widget(list, frame.area(), &mut self.selected); + } + + async fn keypress( + &mut self, + key: KeyEvent, + _state: &mut State, + _command_tx: &ViewSender, + ) -> Result<()> { + match key.code { + KeyCode::Up => self.selected.select_previous(), + KeyCode::Down => self.selected.select_next(), + + _ => {} + } + + Ok(()) + } +} diff --git a/readme.md b/readme.md index fce7c9e..39990c7 100644 --- a/readme.md +++ b/readme.md @@ -6,3 +6,7 @@ - browsing - downloading lectures - support for other echo360 instances +- make it look good +- cache courses +- check for network assets +- download queue