use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Html;
use maud::Markup;
use maud::html;
use crate::cmd::drill::server::AnswerControls;
use crate::cmd::drill::state::MutableState;
use crate::cmd::drill::state::ServerState;
use crate::cmd::drill::template::page_template;
use crate::error::Fallible;
use crate::markdown::MarkdownRenderConfig;
use crate::media::resolve::MediaResolverBuilder;
use crate::types::card::Card;
use crate::types::card::CardType;
pub async fn get_handler(State(state): State<ServerState>) -> (StatusCode, Html<String>) {
let html = match inner(state).await {
Ok(html) => html,
Err(e) => page_template(html! {
div.error {
h1 { "Error" }
p { (e) }
}
}),
};
(StatusCode::OK, Html(html.into_string()))
}
async fn inner(state: ServerState) -> Fallible<Markup> {
let mutable = state.mutable.lock().unwrap();
let body = if mutable.finished_at.is_some() {
render_completion_page(&state, &mutable)?
} else {
render_session_page(&state, &mutable)?
};
let html = page_template(body);
Ok(html)
}
fn render_session_page(state: &ServerState, mutable: &MutableState) -> Fallible<Markup> {
let undo_disabled = mutable.reviews.is_empty();
let total_cards = state.total_cards;
let cards_done = state.total_cards - mutable.cards.len();
let percent_done = if total_cards == 0 {
100
} else {
(cards_done * 100) / total_cards
};
let progress_bar_style = format!("width: {}%;", percent_done);
let card = mutable.cards[0].clone();
let coll_path = state.directory.clone();
let deck_path = card.relative_file_path(&coll_path)?;
let config = MarkdownRenderConfig {
resolver: MediaResolverBuilder::new()
.with_collection_path(coll_path)?
.with_deck_path(deck_path)?
.build()?,
port: state.port,
};
let card_content = render_card(&card, mutable.reveal, &config)?;
let card_controls = if mutable.reveal {
let grades = match state.answer_controls {
AnswerControls::Binary => html! {
input id="forgot" type="submit" name="action" value="Forgot" title="Mark card as forgotten.";
input id="good" type="submit" name="action" value="Good" title="Mark card as remembered.";
},
AnswerControls::Full => html! {
input id="forgot" type="submit" name="action" value="Forgot" title="Mark card as forgotten. Shortcut: 1.";
input id="hard" type="submit" name="action" value="Hard" title="Mark card as difficult. Shortcut: 2.";
input id="good" type="submit" name="action" value="Good" title="Mark card as remembered well. Shortcut: 3.";
input id="easy" type="submit" name="action" value="Easy" title="Mark card as very easy. Shortcut: 4.";
},
};
html! {
form action="/" method="post" {
(undo_button(undo_disabled))
div.spacer {}
div.grades {
(grades)
}
div.spacer {}
(end_button())
}
}
} else {
html! {
form action="/" method="post" {
(undo_button(undo_disabled))
div.spacer {}
input id="reveal" type="submit" name="action" value="Reveal" title="Show the answer. Shortcut: space.";
div.spacer {}
(end_button())
}
}
};
let html = html! {
div.root {
div.header {
div.progress-bar {
div.progress-fill style=(progress_bar_style) {}
}
}
div.card-container {
div.card {
div.card-header {
h1 {
(card.deck_name())
}
}
(card_content)
}
}
div.controls {
(card_controls)
}
}
};
Ok(html)
}
fn render_card(card: &Card, reveal: bool, config: &MarkdownRenderConfig) -> Fallible<Markup> {
let html = match card.card_type() {
CardType::Basic => {
if reveal {
html! {
div .question .rich-text {
(card.html_front(config)?)
}
div .answer .rich-text {
(card.html_back(config)?)
}
}
} else {
html! {
div .question .rich-text {
(card.html_front(config)?)
}
div .answer .rich-text {}
}
}
}
CardType::Cloze => {
if reveal {
html! {
div .prompt .rich-text {
(card.html_back(config)?)
}
}
} else {
html! {
div .prompt .rich-text {
(card.html_front(config)?)
}
}
}
}
};
Ok(html! {
div.card-content {
(html)
}
})
}
const TS_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
fn render_completion_page(state: &ServerState, mutable: &MutableState) -> Fallible<Markup> {
let total_cards = state.total_cards;
let cards_reviewed = state.total_cards - mutable.cards.len();
let start = state.session_started_at.into_inner();
let end = mutable.finished_at.unwrap().into_inner();
let duration_s = (end - start).num_seconds();
let pace: f64 = if cards_reviewed == 0 {
0.0
} else {
duration_s as f64 / cards_reviewed as f64
};
let pace = format!("{:.2}", pace);
let start_ts = start.format(TS_FORMAT).to_string();
let end_ts = end.format(TS_FORMAT).to_string();
let html = html! {
div.finished {
h1 {
"Session Completed 🎉"
}
div.summary {
"Reviewed "
(cards_reviewed)
" cards in "
(duration_s)
" seconds."
}
h2 {
"Session Stats"
}
div.stats {
table {
tbody {
tr {
td .key { "Total Cards" }
td .val { (total_cards) }
}
tr {
td .key { "Cards Reviewed" }
td .val { (cards_reviewed) }
}
tr {
td .key { "Started" }
td .val { (start_ts) }
}
tr {
td .key { "Finished" }
td .val { (end_ts) }
}
tr {
td .key { "Duration (seconds)" }
td .val { (duration_s) }
}
tr {
td .key { "Pace (s/card)" }
td .val { (pace) }
}
}
}
}
div.shutdown-container {
form action="/" method="post" {
input #shutdown .shutdown-button type="submit" name="action" value="Shutdown" title="Shut down the server";
}
}
}
};
Ok(html)
}
fn undo_button(disabled: bool) -> Markup {
if disabled {
html! {
input id="undo" type="submit" name="action" value="Undo" disabled;
}
} else {
html! {
input id="undo" type="submit" name="action" value="Undo" title="Undo last action. Shortcut: u.";
}
}
}
fn end_button() -> Markup {
html! {
input id="end" type="submit" name="action" value="End" title="End the session (changes are saved)";
}
}