feat: list lectures
This commit is contained in:
parent
15bb4f6995
commit
68fb777e5f
8 changed files with 170 additions and 18 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
103
echoed/src/views/lectures.rs
Normal file
103
echoed/src/views/lectures.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -6,3 +6,7 @@
|
|||
- browsing
|
||||
- downloading lectures
|
||||
- support for other echo360 instances
|
||||
- make it look good
|
||||
- cache courses
|
||||
- check for network assets
|
||||
- download queue
|
||||
|
|
Loading…
Reference in a new issue