This commit is contained in:
Michael Zhang 2023-06-11 15:08:15 -05:00
parent 779fc69405
commit 15cbfeb0d6
23 changed files with 4679 additions and 204 deletions

7
.prettierrc.json5 Normal file
View file

@ -0,0 +1,7 @@
{
useTabs: false,
tabWidth: 2,
singleQuote: false,
trailingComma: "all",
printWidth: 100,
}

4481
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,13 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.7.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.3.0",
"classnames": "^2.3.2",
"flowbite": "^1.6.5",
"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",
@ -23,7 +29,10 @@
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
"sass": "^1.62.1", "sass": "^1.62.1",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.2.1" "vite": "^4.2.1"
} }

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

36
src-tauri/Cargo.lock generated
View file

@ -1656,12 +1656,46 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libappindicator"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8"
dependencies = [
"glib",
"gtk",
"gtk-sys",
"libappindicator-sys",
"log",
]
[[package]]
name = "libappindicator-sys"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa"
dependencies = [
"gtk-sys",
"libloading",
"once_cell",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.145" version = "0.2.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81" checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81"
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.24.2" version = "0.24.2"
@ -3048,6 +3082,7 @@ dependencies = [
"core-foundation", "core-foundation",
"core-graphics", "core-graphics",
"crossbeam-channel", "crossbeam-channel",
"dirs-next",
"dispatch", "dispatch",
"gdk", "gdk",
"gdk-pixbuf", "gdk-pixbuf",
@ -3062,6 +3097,7 @@ dependencies = [
"instant", "instant",
"jni", "jni",
"lazy_static", "lazy_static",
"libappindicator",
"libc", "libc",
"log", "log",
"ndk", "ndk",

View file

@ -14,7 +14,7 @@ members = ["database-maker"]
tauri-build = { version = "1.3", features = [] } tauri-build = { version = "1.3", features = [] }
[dependencies] [dependencies]
tauri = { version = "1.3", features = ["shell-open"] } tauri = { version = "1.3", features = ["shell-open", "system-tray"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
dirs = "5.0.1" dirs = "5.0.1"

View file

@ -6,14 +6,24 @@ pub struct KanjiDb(pub SqlitePool);
#[derive(Debug, Derivative, Serialize, Deserialize)] #[derive(Debug, Derivative, Serialize, Deserialize)]
#[derivative(Default)] #[derivative(Default)]
pub struct GetKanjiOptions { pub struct GetKanjiOptions {
/// For looking up an existing one
character: Option<String>,
#[derivative(Default(value = "10"))] #[derivative(Default(value = "10"))]
how_many: u32, how_many: u32,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct Kanji {
character: String,
meaning: String,
most_used_rank: u32,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct GetKanjiResult { pub struct GetKanjiResult {
count: u32, count: u32,
kanji: Vec<String>, kanji: Vec<Kanji>,
} }
#[tauri::command] #[tauri::command]
@ -24,18 +34,33 @@ pub async fn get_kanji(
let opts = options.unwrap_or_default(); let opts = options.unwrap_or_default();
let result = sqlx::query( let result = sqlx::query(
r#"SELECT * FROM KanjiSet r#"
SELECT * FROM KanjiSet
LEFT JOIN KanjiMeaningSet ON KanjiSet.ID = KanjiMeaningSet.Kanji_ID LEFT JOIN KanjiMeaningSet ON KanjiSet.ID = KanjiMeaningSet.Kanji_ID
WHERE MostUsedRank IS NOT NULL GROUP BY KanjiSet.ID
HAVING MostUsedRank IS NOT NULL
ORDER BY MostUsedRank ORDER BY MostUsedRank
LIMIT ?"#, LIMIT ?
"#,
) )
.bind(opts.how_many) .bind(opts.how_many)
.fetch_all(&state.0) .fetch_all(&state.0)
.await .await
.map_err(|_| ())?; .map_err(|_| ())?;
let kanji = result.into_iter().map(|row| row.get("Character")).collect(); let kanji = result
.into_iter()
.map(|row| {
let character = row.get("Character");
let meaning = row.get("Meaning");
let most_used_rank = row.get("MostUsedRank");
Kanji {
character,
meaning,
most_used_rank,
}
})
.collect();
let count = sqlx::query("SELECT COUNT(*) FROM KanjiSet") let count = sqlx::query("SELECT COUNT(*) FROM KanjiSet")
.fetch_one(&state.0) .fetch_one(&state.0)

View file

@ -15,6 +15,7 @@ use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions}, sqlite::{SqliteConnectOptions, SqlitePoolOptions},
SqlitePool, SqlitePool,
}; };
use tauri::SystemTray;
use crate::kanji::KanjiDb; use crate::kanji::KanjiDb;
@ -26,14 +27,20 @@ fn greet(name: &str) -> String {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Open kanji db
let kanji_db_options = let kanji_db_options =
SqliteConnectOptions::from_str("./KanjiDatabase.sqlite")?.read_only(true); SqliteConnectOptions::from_str("./KanjiDatabase.sqlite")?.read_only(true);
let kanji_pool = SqlitePoolOptions::new() let kanji_pool = SqlitePoolOptions::new()
.connect_with(kanji_db_options) .connect_with(kanji_db_options)
.await?; .await?;
// System tray
let tray = SystemTray::new();
// Build tauri
tauri::Builder::default() tauri::Builder::default()
.manage(KanjiDb(kanji_pool)) .manage(KanjiDb(kanji_pool))
.system_tray(tray)
.invoke_handler(tauri::generate_handler![greet, kanji::get_kanji]) .invoke_handler(tauri::generate_handler![greet, kanji::get_kanji])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.context("error while running tauri application")?; .context("error while running tauri application")?;

View file

@ -33,6 +33,10 @@
"security": { "security": {
"csp": null "csp": null
}, },
"systemTray": {
"iconPath": "icons/icon.ico",
"iconAsTemplate": true
},
"windows": [ "windows": [
{ {
"fullscreen": false, "fullscreen": false,

View file

@ -1,11 +1,23 @@
main.main {
display: flex;
flex-direction: column;
}
.header { .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-evenly; justify-content: space-evenly;
list-style-type: none; list-style-type: none;
}
.link {
flex-grow: 1;
display: inline-block;
padding: 12px;
li {
padding-left: 0;
} }
.link-active {
background-color: skyblue;
} }

View file

@ -1,23 +1,35 @@
import { Link, RouterProvider, createHashRouter } from "react-router-dom"; import { Link, RouterProvider, createHashRouter } from "react-router-dom";
import styles from "./App.module.scss";
import KanjiPane from "./panes/KanjiPane"; import KanjiPane from "./panes/KanjiPane";
import classNames from "classnames";
import { ChakraProvider } from '@chakra-ui/react'
import HomePane from "./panes/HomePane"; import HomePane from "./panes/HomePane";
import { createBrowserRouter } from "react-router-dom"; import { createBrowserRouter } from "react-router-dom";
import { Outlet, Route, createRoutesFromElements } from "react-router"; import { Outlet, Route, createRoutesFromElements, useLocation } from "react-router";
import SrsPane from "./panes/SrsPane";
import VocabPane from "./panes/VocabPane";
import SettingsPane from "./panes/SettingsPane";
import { StrictMode } from "react";
import styles from "./App.module.scss";
function Layout() { function Layout() {
const location = useLocation();
return ( return (
<> <main className={styles.main}>
<ul className={styles.header}> <ul className={styles.header}>
{routes.map((route) => ( {routes.map((route) => {
<li key={route.path}> if (!route.title) return undefined;
<Link to={route.path}>{route.title}</Link> const active = route.path == location.pathname;
const className = classNames(styles.link, active && styles['link-active']);
return <li key={route.path}>
<Link to={route.path} className={className}>{route.title}</Link>
</li> </li>
))} })}
</ul> </ul>
<Outlet /> <Outlet />
</> </main>
); );
} }
@ -37,10 +49,18 @@ export default function App() {
) )
); );
return <RouterProvider router={router} />; return <StrictMode>
<ChakraProvider>
<RouterProvider router={router} />
</ChakraProvider>
</StrictMode>
} }
const routes = [ const routes = [
{ path: "/", title: "Home", element: <HomePane /> }, { key: "home", path: "/", title: "Home", element: <HomePane /> },
{ path: "/kanji", title: "Kanji", element: <KanjiPane /> }, { key: "srs", path: "/srs", title: "SRS", element: <SrsPane /> },
{ key: "kanji", path: "/kanji", title: "Kanji", element: <KanjiPane /> },
{ key: "kanjiSelected", path: "/kanji/:selectedKanji", element: <KanjiPane /> },
{ key: "vocab", path: "/vocab", title: "Vocab", element: <VocabPane /> },
{ key: "settings", path: "/settings", title: "Settings", element: <SettingsPane /> },
]; ];

View file

@ -0,0 +1,10 @@
$kanjiDisplaySize: 80px;
.display {
border: 1px solid rgb(87, 87, 210);
width: $kanjiDisplaySize;
height: $kanjiDisplaySize;
font-size: $kanjiDisplaySize * 0.8;
text-align: center;
vertical-align: middle;
}

View file

@ -0,0 +1,19 @@
import { invoke } from "@tauri-apps/api/tauri";
import { GetKanjiResult } from "../panes/KanjiPane";
import { Kanji } from "../types/Kanji"
import styles from "./KanjiDisplay.module.scss"
import useSWR from "swr";
interface KanjiDisplayProps {
kanjiCharacter: string;
}
export default function KanjiDisplay({ kanjiCharacter }: KanjiDisplayProps) {
const { data, error, isLoading } = useSWR(["get_kanji", kanjiCharacter], invoke<GetKanjiResult>);
return <>
<div className={styles.display}>
{kanjiCharacter}
</div>
</>
}

View file

@ -1,3 +0,0 @@
export default function KanjiSearch() {
}

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App"; import App from "./App";
import "./styles.css"; import "./styles.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(

View file

@ -1,10 +1,10 @@
$kanjiDisplaySize: 80px; .kanji-list {
display: flex;
.kanjiDisplay { flex-direction: column;
border: 1px solid rgb(87, 87, 210); gap: 2px;
width: $kanjiDisplaySize; }
height: $kanjiDisplaySize;
font-size: $kanjiDisplaySize * 0.8; .kanji-link {
text-align: center; padding: 4px 8px;
vertical-align: middle; border: 1px solid lightgray;
} }

View file

@ -1,36 +1,57 @@
import { useState } from "react";
import { Kanji } from "../types/Kanji";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import useSWR from "swr"; import useSWR from "swr";
import styles from "./KanjiPane.module.scss"; import { Box, Grid, GridItem, LinkBox, Stack } from "@chakra-ui/layout";
interface GetKanjiResult { import styles from "./KanjiPane.module.scss";
import { Link, useParams } from "react-router-dom";
import KanjiDisplay from "../components/KanjiDisplay";
import { Kanji } from "../types/Kanji";
export interface GetKanjiResult {
count: number; count: number;
kanji: string[]; kanji: Kanji[];
} }
function KanjiList({ data }: { data : GetKanjiResult}) { interface KanjiListProps {
data: GetKanjiResult;
selectedCharacter: string;
}
function KanjiList({ data, selectedCharacter }: KanjiListProps) {
return <> return <>
Displaying {data.kanji.length} of {data.count} results. Displaying {data.kanji.length} of {data.count} results.
<ul> {data.kanji.map(kanji => <Link key={kanji.character} className={styles['kanji-link']} to={`/kanji/${kanji.character}`}>
{data.kanji.map(kanji => <li key={kanji}> <Grid
{kanji} templateRows='repeat(2, 1fr)'
</li>)} templateColumns='1fr 3fr'>
</ul> <GridItem rowSpan={2} style={{ fontSize: '24px', textAlign: 'center' }}>{kanji.character}</GridItem>
<GridItem>{kanji.meaning}</GridItem>
<GridItem>#{kanji.most_used_rank} most used</GridItem>
</Grid>
</Link>)}
</> </>
} }
export default function KanjiPane() { export default function KanjiPane() {
const { selectedKanji } = useParams();
const { data, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>); const { data, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>);
return ( return (
<> <>
{JSON.stringify(error)} {JSON.stringify(error)}
{JSON.stringify(selectedKanji)}
<div className={styles.kanjiDisplay}></div> <Stack spacing={7} direction='row'>
<Box p={2} className={styles['kanji-list']}>
{data && <KanjiList data={data} selectedCharacter={selectedKanji} />}
</Box>
{data && <KanjiList data={data} />} <Box p={5}>
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
</Box>
</Stack >
</> </>
); );
} }

View file

@ -0,0 +1,3 @@
export default function SettingsPane() {
return <></>
}

View file

@ -1,3 +1,3 @@
export default function SrsPane() { export default function SrsPane() {
return <></>
} }

3
src/panes/VocabPane.tsx Normal file
View file

@ -0,0 +1,3 @@
export default function VocabPane() {
return <></>
}

View file

@ -1,109 +1,3 @@
:root { @tailwind base;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; @tailwind components;
font-size: 16px; @tailwind utilities;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

View file

@ -1,4 +1,5 @@
export interface Kanji { export interface Kanji {
character: string; character: string;
translation: string; meaning: string;
most_used_rank: number;
} }

11
tailwind.config.js Normal file
View file

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{html,js,jsx,ts,tsx}",
"./node_modules/flowbite/**/*.js"],
theme: {
extend: {},
},
plugins: [require("flowbite/plugin")],
};