feat: list lectures

This commit is contained in:
newt 2024-11-22 15:36:42 +00:00
parent 15bb4f6995
commit 68fb777e5f
8 changed files with 170 additions and 18 deletions

19
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<Command>;

View file

@ -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<Mutex<dyn View + Send + Sync>>;
pub type ViewContainer = Arc<Mutex<dyn View>>;
#[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)
};
}

View file

@ -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),

View file

@ -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<Course>,
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(())
}
}

View file

@ -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<Utc>,
}
pub struct Lectures {
name: Cow<'static, str>,
section_id: Cow<'static, str>,
lectures: Vec<Lecture>,
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::<ijson::IValue>()
.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::<Option<Vec<_>>>()
})
.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(())
}
}

View file

@ -6,3 +6,7 @@
- browsing
- downloading lectures
- support for other echo360 instances
- make it look good
- cache courses
- check for network assets
- download queue