use axum::Form;
use axum::extract::State;
use axum::response::Redirect;
use serde::Deserialize;
use crate::cmd::drill::state::MutableState;
use crate::cmd::drill::state::Review;
use crate::cmd::drill::state::ServerState;
use crate::db::ReviewRecord;
use crate::error::Fallible;
use crate::fsrs::Grade;
use crate::types::card::Card;
use crate::types::card_hash::CardHash;
use crate::types::performance::Performance;
use crate::types::performance::ReviewedPerformance;
use crate::types::performance::update_performance;
use crate::types::timestamp::Timestamp;
#[derive(Debug, Deserialize)]
enum Action {
Reveal,
Undo,
End,
Forgot,
Hard,
Good,
Easy,
Shutdown,
}
impl Action {
pub fn grade(&self) -> Grade {
match self {
Action::Forgot => Grade::Forgot,
Action::Hard => Grade::Hard,
Action::Good => Grade::Good,
Action::Easy => Grade::Easy,
_ => panic!("Action does not correspond to a grade"),
}
}
}
#[derive(Deserialize)]
pub struct FormData {
action: Action,
}
pub async fn post_handler(
State(state): State<ServerState>,
Form(form): Form<FormData>,
) -> Redirect {
match action_handler(state, form.action).await {
Ok(_) => {}
Err(e) => {
log::error!("error: {e}");
}
}
Redirect::to("/")
}
async fn action_handler(state: ServerState, action: Action) -> Fallible<()> {
let mut mutable = state.mutable.lock().unwrap();
match action {
Action::Reveal => {
if !mutable.reveal {
mutable.reveal = true;
}
}
Action::Undo => {
if !mutable.reviews.is_empty() {
let last_review: Review = mutable.reviews.pop().unwrap();
if last_review.should_repeat() {
mutable.cards.pop();
}
let card: Card = last_review.card;
let hash: CardHash = card.hash();
mutable.cards.insert(0, card);
let performance = mutable.db.get_card_performance(hash)?;
mutable.cache.update(hash, performance)?;
mutable.finished_at = None;
mutable.reveal = false;
}
}
Action::End => {
finish_session(&mut mutable, &state)?;
}
Action::Shutdown => {
if mutable.finished_at.is_some() {
drop(mutable);
let mut shutdown_tx = state.shutdown_tx.lock().unwrap();
if let Some(tx) = shutdown_tx.take() {
let _ = tx.send(());
}
}
}
Action::Forgot | Action::Hard | Action::Good | Action::Easy => {
if mutable.reveal {
let reviewed_at: Timestamp = Timestamp::now();
let card: Card = mutable.cards.remove(0);
let hash: CardHash = card.hash();
let grade: Grade = action.grade();
let performance: Performance = mutable.cache.get(hash)?;
let performance: ReviewedPerformance =
update_performance(performance, grade, reviewed_at);
let review = Review {
card: card.clone(),
reviewed_at,
grade,
stability: performance.stability,
difficulty: performance.difficulty,
interval_raw: performance.interval_raw,
interval_days: performance.interval_days,
due_date: performance.due_date,
};
mutable
.cache
.update(hash, Performance::Reviewed(performance))?;
if review.should_repeat() {
mutable.cards.push(card.clone());
}
mutable.reviews.push(review);
mutable.reveal = false;
if mutable.cards.is_empty() {
finish_session(&mut mutable, &state)?;
}
}
}
}
Ok(())
}
fn finish_session(mutable: &mut MutableState, state: &ServerState) -> Fallible<()> {
log::debug!("Session completed");
let session_ended_at = Timestamp::now();
let reviews: Vec<Review> = mutable.reviews.clone();
let reviews: Vec<ReviewRecord> = reviews.into_iter().map(Review::into_record).collect();
mutable
.db
.save_session(state.session_started_at, session_ended_at, reviews)?;
mutable.finished_at = Some(session_ended_at);
for (card_hash, performance) in mutable.cache.iter() {
mutable
.db
.update_card_performance(*card_hash, *performance)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_grade() {
assert_eq!(Action::Forgot.grade(), Grade::Forgot);
assert_eq!(Action::Hard.grade(), Grade::Hard);
assert_eq!(Action::Good.grade(), Grade::Good);
assert_eq!(Action::Easy.grade(), Grade::Easy);
}
}