diff --git a/src-tauri/src/srs.rs b/src-tauri/src/srs.rs index 2693da0..50915a4 100644 --- a/src-tauri/src/srs.rs +++ b/src-tauri/src/srs.rs @@ -204,15 +204,18 @@ pub async fn generate_review_batch( ) -> Result, String> { let opts = options.unwrap_or_default(); + let utc_now = Ticks::now().map_err(|err| err.to_string())?; let result = sqlx::query( r#" SELECT * FROM SrsEntrySet WHERE AssociatedKanji IS NOT NULL AND CurrentGrade < 8 + AND NextAnswerDate <= ? ORDER BY RANDOM() LIMIT ? "#, ) + .bind(utc_now) .bind(opts.batch_size) .fetch_all(&srs_db.0) .await @@ -256,6 +259,8 @@ pub async fn update_srs_item( }; // Kanji.Interface/ViewModels/Partial/Srs/SrsReviewViewModel.cs:600 + let utc_now = Ticks::now().map_err(|err| err.to_string())?; + let new_answer_date = utc_now + Duration::from_secs(delay as u64); sqlx::query( r#" @@ -263,14 +268,14 @@ pub async fn update_srs_item( SET SuccessCount = SuccessCount + ?, FailureCount = FailureCount + ?, - NextAnswerDate = NextAnswerDate + ?, + NextAnswerDate = ?, CurrentGrade = ? WHERE ID = ? "#, ) .bind(success) .bind(failure) - .bind(delay * TICK_MULTIPLIER) + .bind(new_answer_date) .bind(new_grade) .bind(item_id) .execute(&srs_db.0) diff --git a/src/components/DashboardItemStats.tsx b/src/components/DashboardItemStats.tsx index 6f963de..4c6dafe 100644 --- a/src/components/DashboardItemStats.tsx +++ b/src/components/DashboardItemStats.tsx @@ -39,7 +39,7 @@ export default function DashboardItemStats({ srsStats }: DashboardItemStatsProps

{level.name}

- {grades.get(level.value)} + {grades.get(level.value) ?? 0}
))} diff --git a/src/components/SrsPart.tsx b/src/components/SrsPart.tsx index 6f089c3..114e086 100644 --- a/src/components/SrsPart.tsx +++ b/src/components/SrsPart.tsx @@ -38,9 +38,17 @@ export default function SrsPart({ srsInfo, addSrsItem }: SrsPartProps) { const nextAnswerDate = new Date(srsInfo.next_answer_date); const formatter: Formatter = (value, unit, suffix, epochMilliseconds, nextFormatter) => { - if (epochMilliseconds < Date.now()) return "now"; - return buildFormatter(shortEnStrings)(value, unit, suffix, epochMilliseconds); + if (epochMilliseconds < Date.now()) return <>now; + return buildFormatter(shortEnStrings)( + value, + unit, + suffix, + epochMilliseconds, + nextFormatter, + () => Date.now(), + ); }; + return (
diff --git a/src/components/srsReview/InputBox.tsx b/src/components/srsReview/InputBox.tsx new file mode 100644 index 0000000..33c8610 --- /dev/null +++ b/src/components/srsReview/InputBox.tsx @@ -0,0 +1,88 @@ +import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from "react"; +import { romajiToKana } from "../../lib/kanaHelper"; +import classNames from "classnames"; +import { Grid, GridItem, Input, InputGroup, InputLeftElement } from "@chakra-ui/react"; + +import styles from "./SrsReview.module.scss"; +import { ReviewItemType } from "../../lib/srs"; + +export interface InputBoxProps { + type: ReviewItemType; + answer: string; + setAnswer: (_: string) => void; + incorrectTimes: number; + submit: (_: FormEvent) => Promise; +} + +const displayParams: { [key in ReviewItemType]: [string, string] } = { + [ReviewItemType.MEANING]: ["meaning", "A"], + [ReviewItemType.READING]: ["reading", "あ"], +}; + +export default function InputBox({ + type, + answer, + setAnswer, + incorrectTimes, + submit, +}: InputBoxProps) { + const [focusedBox, setFocusedBox] = useState(null); + + const kanaInput = type == ReviewItemType.READING; + const placeholder = incorrectTimes == 0 ? "Enter your answer..." : "Nope, try again..."; + + useEffect(() => { + if (focusedBox) focusedBox.focus(); + }, [focusedBox]); + + const focusedBoxRef = useCallback( + (node: HTMLInputElement | null) => { + if (node) setFocusedBox(node); + }, + [type], + ); + + return ( + + {Object.values(ReviewItemType).map((thisType: ReviewItemType) => { + const [question, indicator] = displayParams[thisType]; + const enabled = type == thisType; + + const onChange = (evt: ChangeEvent) => { + if (!enabled) return; + let newValue = evt.target.value; + if (kanaInput) newValue = romajiToKana(newValue) ?? newValue; + setAnswer(newValue); + }; + + const inputClassName = classNames( + styles.inputBox, + enabled && incorrectTimes > 0 && styles.incorrect, + ); + + return ( + +

What is the {question}?

+
+ + {indicator} + + enabled && focusedBoxRef(node)} + /> + +
+
+ ); + })} +
+ ); +} diff --git a/src/components/srsReview/SrsReview.module.scss b/src/components/srsReview/SrsReview.module.scss new file mode 100644 index 0000000..43cf31a --- /dev/null +++ b/src/components/srsReview/SrsReview.module.scss @@ -0,0 +1,7 @@ +:not(.activatedQuestion) > .question { + color: rgba(0, 0, 0, 0.5); +} + +.incorrect { + background-color: rgb(255, 202, 202) !important; +} diff --git a/src/panes/SrsReviewPane.module.scss b/src/panes/SrsReviewPane.module.scss index bb2ee98..6566f1b 100644 --- a/src/panes/SrsReviewPane.module.scss +++ b/src/panes/SrsReviewPane.module.scss @@ -17,6 +17,24 @@ padding: 64px 0; } -.incorrect { - background-color: rgb(255, 202, 202) !important; +.needHelp { + margin-top: 16px; + + summary { + cursor: pointer; + } +} + +.possibleAnswers { + padding-left: 12px; + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-start; +} + +.possibleAnswer { + border: 1px solid rgba(0, 0, 0, 0.25); + font-size: 1.1em; + padding: 4px 8px; } diff --git a/src/panes/SrsReviewPane.tsx b/src/panes/SrsReviewPane.tsx index 405666d..b8b83a5 100644 --- a/src/panes/SrsReviewPane.tsx +++ b/src/panes/SrsReviewPane.tsx @@ -27,6 +27,8 @@ import { isGroupCorrect, } from "../lib/srs"; import classNames from "classnames"; +import InputBox from "../components/srsReview/InputBox"; +import SelectOnClick from "../components/utils/SelectOnClick"; const batchSize = 10; @@ -90,7 +92,10 @@ export function Component() { if (!reviewQueue) return ; // Done! Go back to the home page - if (reviewQueue.length == 0) return navigate("/"); + if (reviewQueue.length == 0) { + navigate("/"); + return <>; + } const [nextItem, ...restOfQueue] = reviewQueue; const possibleAnswers = new Set(nextItem.possibleAnswers); @@ -143,41 +148,7 @@ export function Component() { } }; - const inputBox = (kanaInput: boolean) => { - const onChange = (evt: ChangeEvent) => { - let newValue = evt.target.value; - if (kanaInput) newValue = romajiToKana(newValue) ?? newValue; - - setCurrentAnswer(newValue); - }; - - const className = classNames(styles["input-box"], incorrectTimes > 0 && styles["incorrect"]); - const placeholder = - { - 0: "Enter your answer...", - 1: "Wrong! Try again...", - }[incorrectTimes] || `Answer is: ${nextItem.possibleAnswers.join(", ")}`; - - return ( - - {kanaInput ? "あ" : "A"} - - - - ); - }; - const renderInside = () => { - const kanaInput = nextItem.type == ReviewItemType.READING; - return ( <> {startingSize && ( @@ -192,16 +163,26 @@ export function Component() {

{nextItem.challenge}

-
- { - { - [ReviewItemType.MEANING]: "What is the meaning?", - [ReviewItemType.READING]: "What is the reading?", - }[nextItem.type] - } + - {inputBox(kanaInput)} - + {incorrectTimes > 0 && ( +
+ Need help? +
+ {[...possibleAnswers].map((answer) => ( + + {answer} + + ))} +
+
+ )} ); }; @@ -216,7 +197,7 @@ export function Component() { return (
- +