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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
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]]
|
[[package]]
|
||||||
name = "color-eyre"
|
name = "color-eyre"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
@ -307,6 +316,7 @@ name = "echoed"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
@ -1017,6 +1027,15 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
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]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
|
chrono = { version = "0.4.38", default-features = false, features = ["now"] }
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
cookie_store = "0.21.1"
|
cookie_store = "0.21.1"
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
|
|
|
@ -5,5 +5,6 @@ pub use color_eyre::Result;
|
||||||
pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
pub use ratatui::prelude::*;
|
pub use ratatui::prelude::*;
|
||||||
pub use std::any::Any;
|
pub use std::any::Any;
|
||||||
|
pub use std::borrow::Cow;
|
||||||
|
|
||||||
pub type ViewSender = tokio::sync::mpsc::Sender<Command>;
|
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]
|
#[async_trait]
|
||||||
pub trait View {
|
pub trait View: Send + Sync {
|
||||||
async fn setup(&mut self, _state: &State) -> Result<()> {
|
async fn setup(&mut self, _state: &State) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn draw(&self, frame: &mut Frame, state: &State);
|
fn draw(&mut self, frame: &mut Frame, state: &State);
|
||||||
async fn keypress(
|
async fn keypress(
|
||||||
&mut self,
|
&mut self,
|
||||||
_key: KeyEvent,
|
_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_export]
|
||||||
macro_rules! change_view {
|
macro_rules! change_view {
|
||||||
($tx:expr, $view:ident) => {
|
(@ $tx:expr, $value:expr) => {
|
||||||
$tx.send(Command::ChangeView(std::sync::Arc::new(
|
$tx.send(Command::ChangeView(std::sync::Arc::new(
|
||||||
tokio::sync::Mutex::new(super::$view::default()),
|
tokio::sync::Mutex::new($value),
|
||||||
)))
|
)))
|
||||||
.await?
|
.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 echoed::DEFAULT_ECHO360;
|
||||||
use email_address::EmailAddress;
|
use email_address::EmailAddress;
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use std::borrow::Cow;
|
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
const SHOW_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
|
const SHOW_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
|
||||||
|
@ -24,7 +23,7 @@ impl<'t> View for Auth<'t> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame, _: &State) {
|
fn draw(&mut self, frame: &mut Frame, _: &State) {
|
||||||
let area = helper::border(frame, Some("Login"));
|
let area = helper::border(frame, Some("Login"));
|
||||||
let areas = Layout::vertical([
|
let areas = Layout::vertical([
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
use crate::{prelude::*, views::Lectures};
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use echoed::DEFAULT_ECHO360;
|
use echoed::DEFAULT_ECHO360;
|
||||||
|
use ratatui::widgets::{List, ListState};
|
||||||
use crate::prelude::*;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Course {
|
pub struct Course {
|
||||||
|
@ -14,6 +13,7 @@ pub struct Course {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Courses {
|
pub struct Courses {
|
||||||
courses: Vec<Course>,
|
courses: Vec<Course>,
|
||||||
|
selected: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -48,23 +48,42 @@ impl View for Courses {
|
||||||
.ok_or(eyre!("No courses found"))?;
|
.ok_or(eyre!("No courses found"))?;
|
||||||
|
|
||||||
self.courses = enrollments;
|
self.courses = enrollments;
|
||||||
|
self.selected.select_first();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame, _state: &State) {
|
fn draw(&mut self, frame: &mut Frame, _state: &State) {
|
||||||
// let user = state.echo_user.as_ref().unwrap();
|
let items = self
|
||||||
if let Some(x) = self.courses.first() {
|
.courses
|
||||||
frame.render_widget(Line::raw(x.name.to_string()), frame.area());
|
.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(
|
async fn keypress(
|
||||||
&mut self,
|
&mut self,
|
||||||
_key: KeyEvent,
|
key: KeyEvent,
|
||||||
_state: &mut State,
|
_state: &mut State,
|
||||||
_command_tx: &ViewSender,
|
command_tx: &ViewSender,
|
||||||
) -> Result<()> {
|
) -> 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(())
|
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
|
- browsing
|
||||||
- downloading lectures
|
- downloading lectures
|
||||||
- support for other echo360 instances
|
- support for other echo360 instances
|
||||||
|
- make it look good
|
||||||
|
- cache courses
|
||||||
|
- check for network assets
|
||||||
|
- download queue
|
||||||
|
|
Loading…
Reference in a new issue