diff --git a/assets/app.js b/assets/app.js index 7624b51..c0becb8 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1 +1,2 @@ import './styles/app.css'; +import './vendor/bootstrap/dist/css/bootstrap.min.css' diff --git a/assets/styles/app.css b/assets/styles/app.css index e69de29..3d1f1ee 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -0,0 +1,4 @@ +a { + text-decoration: none !important; + color: inherit !important; +} diff --git a/importmap.php b/importmap.php index b73b323..79c546a 100644 --- a/importmap.php +++ b/importmap.php @@ -25,4 +25,8 @@ return [ '@hotwired/turbo' => [ 'version' => '7.3.0', ], + 'bootstrap/dist/css/bootstrap.min.css' => [ + 'version' => '5.3.8', + 'type' => 'css', + ], ]; diff --git a/src/Controller/KanjiController.php b/src/Controller/KanjiController.php new file mode 100644 index 0000000..7d8d4ea --- /dev/null +++ b/src/Controller/KanjiController.php @@ -0,0 +1,64 @@ +anki->getKnownSnKanjiCounts()); + $slnKanji = array_keys($this->anki->getKnownSlnKanjiCounts()); + $unicodeKanji = $this->anki->getUnicodeKanji(); + + $chars = []; + foreach (range(intval("{$start}0", 16), intval("{$end}f", 16)) as $codepoint) { + $charStr = mb_chr($codepoint, 'UTF-8'); + + $chars[] = [ + 'str' => $charStr, + 'codepoint' => dechex($codepoint), + 'lists' => [ + 'sn' => in_array($charStr, $snKanji, true), + 'sln' => in_array($charStr, $slnKanji, true), + 'unicode' => in_array($charStr, $unicodeKanji, true), + ], + ]; + } + + return $this->render(self::tmpl('grid'), [ + 'characters' => $chars, + ]); + } + + #[Route('/{codepoint}/register', 'register', methods: 'GET')] + public function register(string $codepoint): Response + { + // Properly check it's valid (make a util?) + $char = mb_chr(Number::hexint($codepoint), 'UTF-8'); + + $note = UnicodeNote::fromCharacter($char); + $this->anki->addNote($note, UnicodeNote::DECK); + + return new Response(); + } +} diff --git a/src/Entity/UnicodeNote.php b/src/Entity/UnicodeNote.php new file mode 100644 index 0000000..7c92ab6 --- /dev/null +++ b/src/Entity/UnicodeNote.php @@ -0,0 +1,99 @@ +setCharacter($character); + $note->model = self::MODEL_NAME; + return $note; + } + + + // ------------------------------------------------------- Anki-related --- + + public static function fromAnki(array $noteInfo): static + { + $note = parent::fromAnki($noteInfo); + + $note->setCharacter($note->fields['Character']); + + return $note; + } + + /** @return array */ + public function toAnki(): array + { + return array_merge(parent::toAnki(), [ + 'fields' => [ + 'Character' => $this->getCharacter(), + 'Codepoint' => $this->getHex(), + ], + ]); + } + + + // ---------------------------------------------------- Derived methods --- + + /** @throws \RuntimeException */ + public function setCharacter(string $character): static + { + if (mb_strlen($character) !== 1) throw new \InvalidArgumentException(sprintf( + <<setCodepoint($codepoint); + } + + public function getCharacter(): string + { + return mb_chr($this->codepoint, 'UTF-8') + ?: throw new \RuntimeException(sprintf( + 'Codepoint "%s" is not valid', + $this->getHex(), + )); + } + + public function getHex(): string + { + return dechex($this->codepoint); + } + + public function setHex(string $hex): static + { + return $this->setCodepoint(Number::hexint($hex)); + } + + public function setCodepoint(int $codepoint): static + { + if (!mb_chr($codepoint, 'UTF-8')) throw new \InvalidArgumentException(sprintf( + 'Codepoint "%s" is not valid', + $this->getHex(), + )); + + $this->codepoint = $codepoint; + $this->fields['Character'] = $this->getCharacter(); + $this->fields['Codepoint'] = $this->getHex(); + return $this; + } +} diff --git a/src/Service/AnkiService.php b/src/Service/AnkiService.php index 181e49d..9e6a1ae 100644 --- a/src/Service/AnkiService.php +++ b/src/Service/AnkiService.php @@ -5,6 +5,7 @@ namespace App\Service; use App\Entity\Note; use App\Entity\SentenceListeningNote; use App\Entity\SentenceNote; +use App\Entity\UnicodeNote; use App\Utils\Japanese; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -64,6 +65,12 @@ class AnkiService return $this->request('findNotes', ['query' => $query]); } + public function getAllUnicodeNoteIds(): array + { + $query = sprintf('"note:%s"', UnicodeNote::MODEL_NAME); + return $this->request('findNotes', ['query' => $query]); + } + public function getAllSentenceNoteIds(): array { return $this->request( @@ -108,6 +115,7 @@ class AnkiService private static function parseNoteInfo(array $noteInfo): Note { return match ($noteInfo['modelName']) { + UnicodeNote::MODEL_NAME => UnicodeNote::fromAnki($noteInfo), SentenceNote::MODEL_NAME => SentenceNote::fromAnki($noteInfo), SentenceListeningNote::MODEL_NAME => SentenceListeningNote::fromAnki($noteInfo), default => throw new \Exception(sprintf( @@ -133,6 +141,19 @@ class AnkiService $this->request('guiBrowse', ['query' => 'nid:' . $note->getId()]); } + /** @return list */ + public function getUnicodeKanji(): array + { + $ret = []; + + foreach ($this->getNotes($this->getAllUnicodeNoteIds()) as $note) { + assert($note instanceof UnicodeNote); + $ret[] = $note->getCharacter(); + } + + return $ret; + } + /** @return array */ public function getKnownSlnKanjiCounts(?string $order = null): array { diff --git a/src/Utils/Number.php b/src/Utils/Number.php new file mode 100644 index 0000000..f375d0f --- /dev/null +++ b/src/Utils/Number.php @@ -0,0 +1,55 @@ + PHP_INT_MAX) { + } + + $value = hexdec($num); + return is_int($value) ? $value : throw new \OverflowException(sprintf( + 'Hex value "%s" too large for int.', + $num, + )); + } + + public static function tryHexint(string $num): ?int + { + // strip optional prefix + if (str_starts_with($num, '0x') || str_starts_with($num, '0X')) + $num = substr($num, 2); + + if ($num === '' or !ctype_xdigit($num)) return null; + + $value = hexdec($num); + return is_int($value) ? $value : null; + } +} diff --git a/templates/kanji/grid.html.twig b/templates/kanji/grid.html.twig new file mode 100644 index 0000000..79b4ab7 --- /dev/null +++ b/templates/kanji/grid.html.twig @@ -0,0 +1,47 @@ +{% extends 'base.html.twig' %} + +{% block title %}Kanji Index{% endblock %} + +{% block body %} +
+

{{ block('title') }}

+
+ {% for char in characters %} + + {% endfor %} +
+
+{% endblock %}