add stuff to srs
This commit is contained in:
parent
1fe57e2bb6
commit
31728966c5
17 changed files with 485 additions and 133 deletions
58
package-lock.json
generated
58
package-lock.json
generated
|
@ -14,12 +14,14 @@
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
"flowbite": "^1.6.5",
|
"flowbite": "^1.6.5",
|
||||||
"framer-motion": "^10.12.16",
|
"framer-motion": "^10.12.16",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^6.11.2",
|
"react-router": "^6.11.2",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
|
"react-timeago": "^7.1.0",
|
||||||
"swr": "^2.1.5"
|
"swr": "^2.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -27,6 +29,7 @@
|
||||||
"@types/node": "^18.7.10",
|
"@types/node": "^18.7.10",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/react-timeago": "^4.1.3",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
|
@ -2059,6 +2062,15 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-timeago": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-XaaMBzuXLw7lxPPDs/fenlohcf3NDqM5qP4oOL/Meu+Hb1QChW4Igw/SruS1llEqch18RQB3wDTIwvqq4nivvw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
|
@ -2438,6 +2450,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "2.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
|
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.11"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/date-fns"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@ -3520,6 +3547,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-timeago": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
@ -5503,6 +5538,15 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-timeago": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-XaaMBzuXLw7lxPPDs/fenlohcf3NDqM5qP4oOL/Meu+Hb1QChW4Igw/SruS1llEqch18RQB3wDTIwvqq4nivvw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
|
@ -5771,6 +5815,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "2.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
|
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@ -6516,6 +6568,12 @@
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-timeago": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"read-cache": {
|
"read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|
|
@ -16,12 +16,14 @@
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
"flowbite": "^1.6.5",
|
"flowbite": "^1.6.5",
|
||||||
"framer-motion": "^10.12.16",
|
"framer-motion": "^10.12.16",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^6.11.2",
|
"react-router": "^6.11.2",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
|
"react-timeago": "^7.1.0",
|
||||||
"swr": "^2.1.5"
|
"swr": "^2.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -29,6 +31,7 @@
|
||||||
"@types/node": "^18.7.10",
|
"@types/node": "^18.7.10",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/react-timeago": "^4.1.3",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use sqlx::{sqlite::SqliteRow, Encode, Row, SqlitePool, Type};
|
use sqlx::{sqlite::SqliteRow, Encode, Row, SqlitePool, Type};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
srs::SrsDb,
|
||||||
|
utils::{EpochMs, Ticks},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct KanjiDb(pub SqlitePool);
|
pub struct KanjiDb(pub SqlitePool);
|
||||||
|
|
||||||
#[derive(Debug, Derivative, Serialize, Deserialize)]
|
#[derive(Debug, Derivative, Serialize, Deserialize)]
|
||||||
|
@ -12,6 +19,9 @@ pub struct GetKanjiOptions {
|
||||||
#[serde(default = "default_how_many")]
|
#[serde(default = "default_how_many")]
|
||||||
#[derivative(Default(value = "10"))]
|
#[derivative(Default(value = "10"))]
|
||||||
how_many: u32,
|
how_many: u32,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
include_srs_info: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_how_many() -> u32 {
|
fn default_how_many() -> u32 {
|
||||||
|
@ -23,6 +33,7 @@ pub struct Kanji {
|
||||||
character: String,
|
character: String,
|
||||||
most_used_rank: u32,
|
most_used_rank: u32,
|
||||||
meanings: Vec<KanjiMeaning>,
|
meanings: Vec<KanjiMeaning>,
|
||||||
|
srs_info: Option<KanjiSrsInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -37,22 +48,33 @@ pub struct GetKanjiResult {
|
||||||
kanji: Vec<Kanji>,
|
kanji: Vec<Kanji>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KanjiSrsInfo {
|
||||||
|
id: u32,
|
||||||
|
next_answer_date: EpochMs,
|
||||||
|
associated_kanji: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_kanji(
|
pub async fn get_kanji(
|
||||||
db: State<'_, KanjiDb>,
|
kanji_db: State<'_, KanjiDb>,
|
||||||
|
srs_db: State<'_, SrsDb>,
|
||||||
options: Option<GetKanjiOptions>,
|
options: Option<GetKanjiOptions>,
|
||||||
) -> Result<GetKanjiResult, String> {
|
) -> Result<GetKanjiResult, String> {
|
||||||
let opts = options.unwrap_or_default();
|
let opts = options.unwrap_or_default();
|
||||||
println!("Options: {:?}", opts);
|
|
||||||
|
|
||||||
let looking_for_character_clause = match opts.character {
|
let looking_for_character_clause = match opts.character {
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
Some(_) => format!(" AND KanjiSet.Character = ?"),
|
Some(_) => format!("AND KanjiSet.Character = ?"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let query_string = format!(
|
let query_string = format!(
|
||||||
r#"
|
r#"
|
||||||
SELECT *, KanjiMeaningSet.ID as KanjiMeaningID
|
SELECT
|
||||||
|
Character,
|
||||||
|
KanjiMeaningSet.Meaning,
|
||||||
|
MostUsedRank,
|
||||||
|
KanjiMeaningSet.ID as KanjiMeaningID
|
||||||
FROM (
|
FROM (
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM KanjiSet
|
FROM KanjiSet
|
||||||
|
@ -69,21 +91,68 @@ pub async fn get_kanji(
|
||||||
|
|
||||||
// Do all the binds
|
// Do all the binds
|
||||||
|
|
||||||
if let Some(character) = opts.character {
|
if let Some(character) = &opts.character {
|
||||||
println!("Bind {}", character);
|
query = query.bind(character.clone());
|
||||||
query = query.bind(character);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Bind {}", opts.how_many);
|
|
||||||
query = query.bind(opts.how_many);
|
query = query.bind(opts.how_many);
|
||||||
|
|
||||||
println!("Query: {query_string}");
|
|
||||||
|
|
||||||
let result = query
|
let result = query
|
||||||
.fetch_all(&db.0)
|
.fetch_all(&kanji_db.0)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
// Look for SRS info
|
||||||
|
let mut srs_info_map = HashMap::new();
|
||||||
|
if opts.include_srs_info {
|
||||||
|
let looking_for_character_clause = match opts.character {
|
||||||
|
None => String::new(),
|
||||||
|
Some(_) => format!("AND AssociatedKanji = ?"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let query_string = format!(
|
||||||
|
r#"
|
||||||
|
SELECT ID, NextAnswerDate, AssociatedKanji, CurrentGrade FROM SrsEntrySet
|
||||||
|
WHERE 1=1
|
||||||
|
{}
|
||||||
|
"#,
|
||||||
|
looking_for_character_clause
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut query = sqlx::query(&query_string);
|
||||||
|
|
||||||
|
if let Some(character) = &opts.character {
|
||||||
|
query = query.bind(character.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = query
|
||||||
|
.fetch_all(&srs_db.0)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
for row in result {
|
||||||
|
let associated_kanji: String = match row.get("AssociatedKanji") {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = row.get("ID");
|
||||||
|
let next_answer_date: i64 = row.get("NextAnswerDate");
|
||||||
|
let next_answer_date = Ticks(next_answer_date).epoch_ms();
|
||||||
|
|
||||||
|
srs_info_map.insert(
|
||||||
|
associated_kanji.clone(),
|
||||||
|
KanjiSrsInfo {
|
||||||
|
id,
|
||||||
|
next_answer_date,
|
||||||
|
associated_kanji,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("SRS MAP: {srs_info_map:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Put it all together
|
||||||
let kanji = {
|
let kanji = {
|
||||||
let mut new_vec: Vec<Kanji> = Vec::with_capacity(result.len());
|
let mut new_vec: Vec<Kanji> = Vec::with_capacity(result.len());
|
||||||
let mut last_character: Option<String> = None;
|
let mut last_character: Option<String> = None;
|
||||||
|
@ -110,10 +179,13 @@ pub async fn get_kanji(
|
||||||
if let Some(kanji) = same_as {
|
if let Some(kanji) = same_as {
|
||||||
kanji.meanings.push(meaning);
|
kanji.meanings.push(meaning);
|
||||||
} else {
|
} else {
|
||||||
|
let srs_info = srs_info_map.remove(&character);
|
||||||
|
|
||||||
new_vec.push(Kanji {
|
new_vec.push(Kanji {
|
||||||
character,
|
character,
|
||||||
most_used_rank,
|
most_used_rank,
|
||||||
meanings: vec![meaning],
|
meanings: vec![meaning],
|
||||||
|
srs_info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,10 +193,8 @@ pub async fn get_kanji(
|
||||||
new_vec
|
new_vec
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Result: {kanji:?}");
|
|
||||||
|
|
||||||
let count = sqlx::query("SELECT COUNT(*) FROM KanjiSet")
|
let count = sqlx::query("SELECT COUNT(*) FROM KanjiSet")
|
||||||
.fetch_one(&db.0)
|
.fetch_one(&kanji_db.0)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
let count = count.try_get(0).map_err(|err| err.to_string())?;
|
let count = count.try_get(0).map_err(|err| err.to_string())?;
|
||||||
|
|
|
@ -6,8 +6,9 @@ extern crate derivative;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
|
||||||
mod kanji;
|
pub mod kanji;
|
||||||
mod srs;
|
pub mod srs;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -61,6 +62,7 @@ async fn main() -> Result<()> {
|
||||||
.system_tray(tray)
|
.system_tray(tray)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
srs::get_srs_stats,
|
srs::get_srs_stats,
|
||||||
|
srs::add_srs_item,
|
||||||
kanji::get_kanji,
|
kanji::get_kanji,
|
||||||
])
|
])
|
||||||
.on_window_event(|event| match event.event() {
|
.on_window_event(|event| match event.event() {
|
||||||
|
|
|
@ -6,6 +6,8 @@ use std::{
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::utils::Ticks;
|
||||||
|
|
||||||
pub struct SrsDb(pub SqlitePool);
|
pub struct SrsDb(pub SqlitePool);
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -38,17 +40,8 @@ pub async fn get_srs_stats(db: State<'_, SrsDb>) -> Result<SrsStats, String> {
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
// reviews query
|
// reviews query
|
||||||
let utc_now = SystemTime::now()
|
let utc_now = Ticks::now().map_err(|err| err.to_string())?;
|
||||||
.duration_since(UNIX_EPOCH)
|
let utc_tomorrow = utc_now + Duration::from_secs(60 * 60 * 24);
|
||||||
.map_err(|err| err.to_string())?
|
|
||||||
.as_secs() as i64
|
|
||||||
* 1000000000;
|
|
||||||
let utc_tomorrow = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map_err(|err| err.to_string())?
|
|
||||||
.add(Duration::from_secs(60 * 60 * 24))
|
|
||||||
.as_secs() as i64
|
|
||||||
* 1000000000;
|
|
||||||
let row2 = sqlx::query(
|
let row2 = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT COUNT(*) AS reviews
|
SELECT COUNT(*) AS reviews
|
||||||
|
@ -79,3 +72,38 @@ pub async fn get_srs_stats(db: State<'_, SrsDb>) -> Result<SrsStats, String> {
|
||||||
num_failure: row.try_get("num_failure").unwrap_or(0),
|
num_failure: row.try_get("num_failure").unwrap_or(0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Derivative, Serialize, Deserialize)]
|
||||||
|
#[derivative(Default)]
|
||||||
|
pub struct AddSrsItemOptions {
|
||||||
|
character: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_srs_item(
|
||||||
|
db: State<'_, SrsDb>,
|
||||||
|
options: Option<AddSrsItemOptions>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let opts = options.unwrap_or_default();
|
||||||
|
println!("Opts: {opts:?}");
|
||||||
|
|
||||||
|
let query_string = format!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO SrsEntrySet
|
||||||
|
(CreationDate, AssociatedKanji, NextAnswerDate,
|
||||||
|
Meanings, Readings)
|
||||||
|
VALUES
|
||||||
|
(?, ?, ?, '', '')
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
let utc_now = Ticks::now().map_err(|err| err.to_string())?;
|
||||||
|
let query = sqlx::query(&query_string)
|
||||||
|
.bind(&utc_now)
|
||||||
|
.bind(&opts.character)
|
||||||
|
.bind(&utc_now);
|
||||||
|
|
||||||
|
query.execute(&db.0).await.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
66
src-tauri/src/utils.rs
Normal file
66
src-tauri/src/utils.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use std::{
|
||||||
|
ops::Add,
|
||||||
|
time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use sqlx::{
|
||||||
|
database::{HasArguments, HasValueRef},
|
||||||
|
encode::IsNull,
|
||||||
|
error::BoxDynError,
|
||||||
|
Decode, Encode, Sqlite, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Ticks(pub i64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct EpochMs(pub u64);
|
||||||
|
|
||||||
|
impl Ticks {
|
||||||
|
pub fn now() -> Result<Self, SystemTimeError> {
|
||||||
|
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn epoch_ms(&self) -> EpochMs {
|
||||||
|
let millis = Duration::from_nanos(self.0 as u64).as_millis() as u64;
|
||||||
|
EpochMs(millis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Duration> for Ticks {
|
||||||
|
fn into(self) -> Duration {
|
||||||
|
Duration::from_nanos(self.0 as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Duration> for Ticks {
|
||||||
|
fn from(value: Duration) -> Self {
|
||||||
|
Ticks(value.as_secs() as i64 * 1_000_000_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'q> Encode<'q, Sqlite> for Ticks {
|
||||||
|
fn encode_by_ref(
|
||||||
|
&self,
|
||||||
|
buf: &mut <Sqlite as HasArguments<'q>>::ArgumentBuffer,
|
||||||
|
) -> IsNull {
|
||||||
|
self.0.encode_by_ref(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Type<Sqlite> for Ticks {
|
||||||
|
fn type_info() -> <Sqlite as sqlx::Database>::TypeInfo {
|
||||||
|
i64::type_info()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add<Duration> for Ticks {
|
||||||
|
type Output = Ticks;
|
||||||
|
|
||||||
|
fn add(self, rhs: Duration) -> Self::Output {
|
||||||
|
let rhs_ticks = Ticks::from(rhs);
|
||||||
|
Ticks(self.0 + rhs_ticks.0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
$navLinkAccentColor: #09c;
|
||||||
|
$navLinkColor: hsl(203, 91%, 91%);
|
||||||
|
|
||||||
main.main {
|
main.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -22,10 +25,20 @@ main.main {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
|
transition: background-color 0.1s ease-out, border-top-color 0.1s ease-out;
|
||||||
|
|
||||||
|
// Make the top bar seem less web-ish
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
|
||||||
|
&:not(.link-active):hover {
|
||||||
|
border-top-color: transparent;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-active {
|
.link-active {
|
||||||
border-top-color: #09c;
|
border-top-color: $navLinkAccentColor;
|
||||||
background-color: hsl(203, 91%, 91%);
|
background-color: $navLinkColor;
|
||||||
}
|
}
|
||||||
|
|
13
src/App.tsx
13
src/App.tsx
|
@ -2,7 +2,6 @@ import { Link, RouterProvider, createHashRouter } from "react-router-dom";
|
||||||
import KanjiPane from "./panes/KanjiPane";
|
import KanjiPane from "./panes/KanjiPane";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ChakraProvider } from "@chakra-ui/react";
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
import HomePane from "./panes/HomePane";
|
|
||||||
import { createBrowserRouter } from "react-router-dom";
|
import { createBrowserRouter } from "react-router-dom";
|
||||||
import { Outlet, Route, createRoutesFromElements, matchPath, useLocation } from "react-router";
|
import { Outlet, Route, createRoutesFromElements, matchPath, useLocation } from "react-router";
|
||||||
import SrsPane from "./panes/SrsPane";
|
import SrsPane from "./panes/SrsPane";
|
||||||
|
@ -25,8 +24,9 @@ function Layout() {
|
||||||
).some((item) => matchPath({ path: item.path }, location.pathname));
|
).some((item) => matchPath({ path: item.path }, location.pathname));
|
||||||
const mainPath = navLink.subPaths ? navLink.subPaths[0].path : navLink.path;
|
const mainPath = navLink.subPaths ? navLink.subPaths[0].path : navLink.path;
|
||||||
const className = classNames(styles.link, active && styles["link-active"]);
|
const className = classNames(styles.link, active && styles["link-active"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={navLink.path}>
|
<li key={`navLink-${navLink.key}`}>
|
||||||
<Link to={mainPath} className={className}>
|
<Link to={mainPath} className={className}>
|
||||||
{navLink.title}
|
{navLink.title}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -49,7 +49,7 @@ export default function App() {
|
||||||
return route.subPaths.map((subRoute, idx2) => {
|
return route.subPaths.map((subRoute, idx2) => {
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
key={`${route.key}-${subRoute.key}`}
|
key={`route-${route.key}-${subRoute.key}`}
|
||||||
index={idx + idx2 == 0}
|
index={idx + idx2 == 0}
|
||||||
path={subRoute.path}
|
path={subRoute.path}
|
||||||
element={subRoute.element ?? route.element}
|
element={subRoute.element ?? route.element}
|
||||||
|
@ -58,7 +58,12 @@ export default function App() {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Route key={route.path} index={idx == 0} path={route.path} element={route.element} />
|
<Route
|
||||||
|
key={`route-${route.key}`}
|
||||||
|
index={idx == 0}
|
||||||
|
path={route.path}
|
||||||
|
element={route.element}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -29,6 +29,11 @@ interface SrsStats {
|
||||||
num_failure: number;
|
num_failure: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Stat {
|
||||||
|
label: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
export default function DashboardReviewStats() {
|
export default function DashboardReviewStats() {
|
||||||
const {
|
const {
|
||||||
data: srsStats,
|
data: srsStats,
|
||||||
|
@ -44,12 +49,12 @@ export default function DashboardReviewStats() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const averageSuccess = srsStats.num_success / ((srsStats.num_success + srsStats.num_failure) || 1);
|
const averageSuccess = srsStats.num_success / (srsStats.num_success + srsStats.num_failure || 1);
|
||||||
const averageSuccessStr = `${Math.round(averageSuccess * 10000) / 100}%`;
|
const averageSuccessStr = `${Math.round(averageSuccess * 10000) / 100}%`;
|
||||||
|
|
||||||
const canReview = srsStats.reviews_available == 0;
|
const canReview = srsStats.reviews_available == 0;
|
||||||
|
|
||||||
const generateStat = (stat) => {
|
const generateStat = (stat: Stat) => {
|
||||||
return (
|
return (
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Stat>
|
<Stat>
|
||||||
|
@ -67,12 +72,16 @@ export default function DashboardReviewStats() {
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatLabel>reviews available</StatLabel>
|
<StatLabel>reviews available</StatLabel>
|
||||||
<StatNumber>{srsStats.reviews_available}</StatNumber>
|
<StatNumber>{srsStats.reviews_available}</StatNumber>
|
||||||
<ConditionalWrapper condition={canReview} wrapper={children => <Tooltip label="Add items to start reviewing">{children}</Tooltip>}>
|
<ConditionalWrapper
|
||||||
<Link to="/srs/review">
|
condition={canReview}
|
||||||
<Button isDisabled={canReview} colorScheme="blue">
|
wrapper={(children) => (
|
||||||
Start reviewing <ArrowRightIcon marginLeft={3} />
|
<Tooltip label="Add items to start reviewing">{children}</Tooltip>
|
||||||
</Button>
|
)}
|
||||||
</Link>
|
elseWrapper={(children) => <Link to="/srs/review">{children}</Link>}
|
||||||
|
>
|
||||||
|
<Button isDisabled={canReview} colorScheme="blue">
|
||||||
|
Start reviewing <ArrowRightIcon marginLeft={3} />
|
||||||
|
</Button>
|
||||||
</ConditionalWrapper>
|
</ConditionalWrapper>
|
||||||
</Stat>
|
</Stat>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { invoke } from "@tauri-apps/api/tauri";
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
import { GetKanjiResult } from "../panes/KanjiPane";
|
import { GetKanjiResult } from "../panes/KanjiPane";
|
||||||
import { Kanji } from "../types/Kanji";
|
import { Kanji } from "../types/Kanji";
|
||||||
|
import TimeAgo from "react-timeago";
|
||||||
import styles from "./KanjiDisplay.module.scss";
|
import styles from "./KanjiDisplay.module.scss";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Button } from "@chakra-ui/button";
|
import { Button } from "@chakra-ui/button";
|
||||||
import { AddIcon } from "@chakra-ui/icons";
|
import { AddIcon } from "@chakra-ui/icons";
|
||||||
|
import SelectOnClick from "../lib/SelectOnClick";
|
||||||
|
import { Alert, AlertIcon } from "@chakra-ui/alert";
|
||||||
|
|
||||||
interface KanjiDisplayProps {
|
interface KanjiDisplayProps {
|
||||||
kanjiCharacter: string;
|
kanjiCharacter: string;
|
||||||
|
@ -15,30 +18,59 @@ export default function KanjiDisplay({ kanjiCharacter }: KanjiDisplayProps) {
|
||||||
data: kanjiResult,
|
data: kanjiResult,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
mutate,
|
||||||
} = useSWR(["get_kanji", kanjiCharacter], ([command, character]) =>
|
} = useSWR(["get_kanji", kanjiCharacter], ([command, character]) =>
|
||||||
invoke<GetKanjiResult>(command, { options: { character } }),
|
invoke<GetKanjiResult>(command, { options: { character, include_srs_info: true } }),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!kanjiResult || !kanjiResult.kanji) return <>
|
if (!kanjiResult || !kanjiResult.kanji)
|
||||||
{JSON.stringify([kanjiResult, error, isLoading])}
|
return (
|
||||||
Loading...
|
<>
|
||||||
</>;
|
{JSON.stringify([kanjiResult, error, isLoading])}
|
||||||
|
Loading...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const kanji = kanjiResult.kanji[0];
|
const kanji = kanjiResult.kanji[0];
|
||||||
|
|
||||||
const addSrsItem = () => {
|
const addSrsItem = async () => {
|
||||||
|
await invoke("add_srs_item", {
|
||||||
|
options: {
|
||||||
|
character: kanji.character,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let srsPart = (
|
||||||
|
<Button onClick={addSrsItem} colorScheme="green">
|
||||||
|
<AddIcon /> Add to SRS
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
if (kanji.srs_info) {
|
||||||
|
const nextAnswerDate = new Date(kanji.srs_info.next_answer_date);
|
||||||
|
srsPart = (
|
||||||
|
<Alert status="info">
|
||||||
|
<AlertIcon /> <p>This character is being tracked in SRS!</p>
|
||||||
|
<p>
|
||||||
|
(Next test: <TimeAgo date={nextAnswerDate} />)
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.display}>{kanji.character}</div>
|
<details>
|
||||||
|
<summary>Debug</summary>
|
||||||
|
<pre>{JSON.stringify(kanji, null, 2)}</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
{kanji.meanings.map(m => m.meaning).join(", ")}
|
<SelectOnClick className={styles.display}>{kanji.character}</SelectOnClick>
|
||||||
|
|
||||||
<Button onClick={addSrsItem} colorScheme="green">
|
{kanji.meanings.map((m) => m.meaning).join(", ")}
|
||||||
<AddIcon /> Add to SRS
|
|
||||||
</Button>
|
<div>{srsPart}</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
29
src/components/KanjiList.module.scss
Normal file
29
src/components/KanjiList.module.scss
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.kanji-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanji-link {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanji-list-scroll {
|
||||||
|
direction: rtl;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanji-list-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanji-link-active {
|
||||||
|
border-left-color: #09c;
|
||||||
|
background-color: hsl(203, 91%, 91%);
|
||||||
|
}
|
47
src/components/KanjiList.tsx
Normal file
47
src/components/KanjiList.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { GetKanjiResult } from "../panes/KanjiPane";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Badge, Grid, GridItem } from "@chakra-ui/layout";
|
||||||
|
import styles from "./KanjiList.module.scss";
|
||||||
|
import { Kanji } from "../types/Kanji";
|
||||||
|
|
||||||
|
export interface KanjiListProps {
|
||||||
|
data: GetKanjiResult;
|
||||||
|
selectedCharacter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
|
||||||
|
const renderKanjiItem = (kanji: Kanji, active: boolean) => {
|
||||||
|
const className = classNames(styles["kanji-link"], active && styles["kanji-link-active"]);
|
||||||
|
return (
|
||||||
|
<Link key={kanji.character} className={className} to={`/kanji/${kanji.character}`}>
|
||||||
|
<Grid templateRows="repeat(2, 1fr)" templateColumns="1fr 3fr">
|
||||||
|
<GridItem rowSpan={2} style={{ fontSize: "24px", textAlign: "center" }}>
|
||||||
|
{kanji.character}
|
||||||
|
</GridItem>
|
||||||
|
<GridItem>{kanji.meanings[0].meaning}</GridItem>
|
||||||
|
<GridItem>
|
||||||
|
<Badge>#{kanji.most_used_rank} most used</Badge>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<small>
|
||||||
|
Displaying {data.kanji.length} of {data.count} results.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div className={styles["kanji-list-scroll"]}>
|
||||||
|
<div className={styles["kanji-list-inner"]}>
|
||||||
|
{data.kanji.map((kanji) => {
|
||||||
|
const active = kanji.character == selectedCharacter;
|
||||||
|
return renderKanjiItem(kanji, active);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,21 @@
|
||||||
export default function ConditionalWrapper({ condition, wrapper, children }) {
|
export interface ConditionalWrapperProps<S, T> {
|
||||||
return condition ? wrapper(children) : children
|
condition: boolean;
|
||||||
|
wrapper?: (_: S) => T;
|
||||||
|
elseWrapper?: (_: S) => T;
|
||||||
|
children: S;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConditionalWrapper<S, T>({
|
||||||
|
condition,
|
||||||
|
wrapper,
|
||||||
|
children,
|
||||||
|
elseWrapper,
|
||||||
|
}: ConditionalWrapperProps<S, T>) {
|
||||||
|
return condition
|
||||||
|
? wrapper
|
||||||
|
? wrapper(children)
|
||||||
|
: children
|
||||||
|
: elseWrapper
|
||||||
|
? elseWrapper(children)
|
||||||
|
: children;
|
||||||
}
|
}
|
30
src/lib/SelectOnClick.tsx
Normal file
30
src/lib/SelectOnClick.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { HTMLAttributes, PropsWithChildren, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export default function SelectOnClick({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<HTMLAttributes<HTMLDivElement>>) {
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const containerRef = useCallback((element: HTMLDivElement | null) => {
|
||||||
|
if (element) setContainer(element);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(container);
|
||||||
|
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel) return;
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}, [container]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClick={onClick} ref={containerRef} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
.kanji-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanji-link {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanji-list-scroll {
|
|
||||||
direction: rtl;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanji-list-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanji-link-active {
|
|
||||||
border-left-color: #09c;
|
|
||||||
background-color: hsl(203, 91%, 91%);
|
|
||||||
}
|
|
|
@ -7,68 +7,32 @@ import { Link, useParams } from "react-router-dom";
|
||||||
import KanjiDisplay from "../components/KanjiDisplay";
|
import KanjiDisplay from "../components/KanjiDisplay";
|
||||||
import { Kanji } from "../types/Kanji";
|
import { Kanji } from "../types/Kanji";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { KanjiList } from "../components/KanjiList";
|
||||||
|
|
||||||
export interface GetKanjiResult {
|
export interface GetKanjiResult {
|
||||||
count: number;
|
count: number;
|
||||||
kanji: Kanji[];
|
kanji: Kanji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KanjiListProps {
|
|
||||||
data: GetKanjiResult;
|
|
||||||
selectedCharacter?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function KanjiList({ data, selectedCharacter }: KanjiListProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<small>
|
|
||||||
Displaying {data.kanji.length} of {data.count} results.
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<div className={styles["kanji-list-scroll"]}>
|
|
||||||
<div className={styles["kanji-list-inner"]}>
|
|
||||||
{data.kanji.map((kanji) => {
|
|
||||||
const active = kanji.character == selectedCharacter;
|
|
||||||
const className = classNames(styles['kanji-link'], active && styles['kanji-link-active'])
|
|
||||||
return <Link
|
|
||||||
key={kanji.character}
|
|
||||||
className={className}
|
|
||||||
to={`/kanji/${kanji.character}`}
|
|
||||||
>
|
|
||||||
<Grid templateRows="repeat(2, 1fr)" templateColumns="1fr 3fr">
|
|
||||||
<GridItem rowSpan={2} style={{ fontSize: "24px", textAlign: "center" }}>
|
|
||||||
{kanji.character}
|
|
||||||
</GridItem>
|
|
||||||
<GridItem>{kanji.meanings[0].meaning}</GridItem>
|
|
||||||
<GridItem>
|
|
||||||
<small>
|
|
||||||
#{kanji.most_used_rank} most used
|
|
||||||
</small>
|
|
||||||
</GridItem>
|
|
||||||
</Grid>
|
|
||||||
</Link>
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function KanjiPane() {
|
export default function KanjiPane() {
|
||||||
const { selectedKanji } = useParams();
|
const { selectedKanji } = useParams();
|
||||||
const { data, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>);
|
const { data, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing={7} direction="row">
|
<Grid templateRows="1fr" templateColumns="3fr 5fr">
|
||||||
<Box p={2} className={styles["kanji-list"]}>
|
<GridItem className={styles["kanji-list"]}>
|
||||||
{data && <KanjiList data={data} selectedCharacter={selectedKanji} />}
|
{data && <KanjiList data={data} selectedCharacter={selectedKanji} />}
|
||||||
</Box>
|
</GridItem>
|
||||||
|
|
||||||
<Box p={5}>
|
<GridItem>
|
||||||
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
|
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
|
||||||
</Box>
|
</GridItem>
|
||||||
</Stack>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,16 @@ export interface Kanji {
|
||||||
character: string;
|
character: string;
|
||||||
most_used_rank: number;
|
most_used_rank: number;
|
||||||
meanings: KanjiMeaning[];
|
meanings: KanjiMeaning[];
|
||||||
|
srs_info?: KanjiSrsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KanjiMeaning {
|
export interface KanjiMeaning {
|
||||||
id: number;
|
id: number;
|
||||||
meaning: string;
|
meaning: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KanjiSrsInfo {
|
||||||
|
id: number;
|
||||||
|
next_answer_date: number;
|
||||||
|
associated_kanji: string;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue