feat: Implement individual kanji view with niceties
This commit is contained in:
parent
b67434abda
commit
649cecfa36
|
@ -5,6 +5,7 @@ namespace App\Controller;
|
|||
use App\Entity\UnicodeNote;
|
||||
use App\Service\AnkiService;
|
||||
use App\Service\CharListService;
|
||||
use App\Utils\Japanese;
|
||||
use App\Utils\Number;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -79,10 +80,6 @@ class KanjiController extends AbstractController
|
|||
string $end,
|
||||
Request $request,
|
||||
): Response {
|
||||
$jiten = json_decode(file_get_contents(
|
||||
"{$this->getParameter('kernel.project_dir')}/data/kanken-links.json",
|
||||
), true);
|
||||
|
||||
$charInfo = $this->getCharInfo(force: $request->isNoCache());
|
||||
|
||||
$chars = [];
|
||||
|
@ -115,4 +112,31 @@ class KanjiController extends AbstractController
|
|||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
#[Route('/{char}/view', 'view', methods: 'GET')]
|
||||
public function view(string $char): Response
|
||||
{
|
||||
$charStr = ctype_xdigit($char) ? Number::parseCodepoint($char) : $char;
|
||||
$codepoint = ctype_xdigit($char) ? $char : dechex(mb_ord($char));
|
||||
|
||||
$charInfo = $this->getCharInfo()[$charStr] ?? [];
|
||||
//$listTitles = $this->charList->getTitles();
|
||||
|
||||
$jiten = json_decode(file_get_contents(
|
||||
"{$this->getParameter('kernel.project_dir')}/data/kanken-links.json",
|
||||
), true);
|
||||
|
||||
|
||||
$ebookRef = require "$this->varBasepath/ebook-ref.php";
|
||||
|
||||
return $this->render(self::tmpl('view'), [
|
||||
'char' => $charStr,
|
||||
'codepoint' => $codepoint,
|
||||
'info' => $charInfo,
|
||||
'ref' => $ebookRef[$charStr] ?? [],
|
||||
'jiten_href' => key_exists($charStr, $jiten)
|
||||
? "https://kanji.jitenon.jp/kanji{$jiten[$charStr]}"
|
||||
: null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Service;
|
||||
|
||||
use App\Utils\Japanese;
|
||||
use DirectoryIterator;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class CharListService
|
||||
{
|
||||
|
@ -15,7 +17,7 @@ class CharListService
|
|||
*
|
||||
* @return array<string, string|array<string, 0>|array<string|array<string, 0>>>
|
||||
*/
|
||||
function getList(string $name): array
|
||||
public function getList(string $name): array
|
||||
{
|
||||
if (!ctype_alnum($name)) throw new \Exception(sprintf(
|
||||
'Invalid name for list "%s". Only alphanumeric characters allowed',
|
||||
|
@ -68,6 +70,25 @@ class CharListService
|
|||
return $ret;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function getTitles(): array
|
||||
{
|
||||
$ret = [];
|
||||
|
||||
$dirIter = new DirectoryIterator($this->basepath);
|
||||
foreach ($dirIter as $entry) {
|
||||
if (
|
||||
$entry->isDot() or
|
||||
!str_ends_with($entry->getFilename(), ".list")
|
||||
) continue;
|
||||
|
||||
$file = $entry->openFile();
|
||||
$ret[$entry->getBasename('.list')] = $file->fgets();
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array on success, true on non-block
|
||||
*
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\AppExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class AppExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
// If your filter generates SAFE HTML, you should add a third
|
||||
// parameter: ['is_safe' => ['html']]
|
||||
// Reference: https://twig.symfony.com/doc/3.x/advanced.html#automatic-escaping
|
||||
new TwigFilter('basename', basename(...)),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
//new TwigFunction('function_name', [AppExtensionRuntime::class, 'doSomething']),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class AppExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Inject dependencies if needed
|
||||
}
|
||||
|
||||
public function doSomething($value)
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
|
@ -52,4 +52,10 @@ class Number
|
|||
$value = hexdec($num);
|
||||
return is_int($value) ? $value : null;
|
||||
}
|
||||
|
||||
public static function parseCodepoint(int|string $char): string
|
||||
{
|
||||
$charInt = is_int($char) ? $char : self::hexint($char);
|
||||
return mb_chr($charInt, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,5 +13,67 @@
|
|||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<div
|
||||
class="modal fade modal-xl"
|
||||
id="baseModal"
|
||||
tabindex="-1"
|
||||
aria-labelledby="baseModalLabel"
|
||||
aria-hidden="true"
|
||||
hx-indicator="#baseModalIndicator"
|
||||
>
|
||||
<div class="modal-dialog" style="position: relative;">
|
||||
|
||||
<div
|
||||
class="modal-content"
|
||||
id="baseModalContent"
|
||||
style="min-height: 400px;"
|
||||
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#baseModalIndicator"
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1
|
||||
class="modal-title fs-5"
|
||||
id="baseModalLabel"
|
||||
>
|
||||
Modal
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" style="min-height: 350px;"></div>
|
||||
</div>
|
||||
|
||||
{#<div
|
||||
id="baseModalIndicator"
|
||||
class="display-indicator"
|
||||
style="
|
||||
background-color: rgba(255,255,255,0.75);
|
||||
display: block !important;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
"
|
||||
>
|
||||
<div class="d-flex flex-column align-items-center justify-content-center h-100 gap-3">
|
||||
<div style="width: 3rem; height: 3rem;" class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
</div>#}
|
||||
|
||||
</div>
|
||||
</div> <!-- modal -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -45,7 +45,12 @@
|
|||
|
||||
<div class="example-wrapper">
|
||||
<h1 class="text-center p-2 mb-2">{{ block('title') }}</h1>
|
||||
<div class="d-flex flex-wrap m-2">
|
||||
<div
|
||||
class="d-flex flex-wrap m-2"
|
||||
|
||||
hx-target="#baseModalContent"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{% for char in characters %}
|
||||
<div style="width: calc(100% / 16);">
|
||||
<div
|
||||
|
@ -56,8 +61,15 @@
|
|||
"
|
||||
title="{{ char.lists|keys|join("\n") }}"
|
||||
>
|
||||
<div class="kanji-card border-bottom fs-4 pb-1">
|
||||
{% if char.jiten_href != null %}
|
||||
<div
|
||||
class="kanji-card border-bottom fs-1 pb-1"
|
||||
|
||||
hx-get="{{ path('app_kanji_view', { char: char.codepoint }) }}"
|
||||
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#baseModal"
|
||||
>
|
||||
{#{% if char.jiten_href != null %}
|
||||
<a
|
||||
target="_blank"
|
||||
href="{{- char.jiten_href -}}"
|
||||
|
@ -65,14 +77,12 @@
|
|||
>
|
||||
{{- char.str -}}
|
||||
</a>
|
||||
{% else %}
|
||||
{% else %}#}
|
||||
{{- char.str -}}
|
||||
{% endif %}
|
||||
{#{% endif %}#}
|
||||
|
||||
</div>
|
||||
<div style="
|
||||
font-size: 0.5em;
|
||||
padding-bottom: 2px;
|
||||
">
|
||||
<div style="padding-bottom: 2px;">
|
||||
{% if char.lists.unicode is not defined %}
|
||||
<a
|
||||
target="_blank"
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<div class="d-flex flex-column gap-2 p-3 align-items-start">
|
||||
<div class="d-flex gap-2 w-100">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div style="font-size: 6em;">{{ char }}</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
{{ codepoint }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h2>Lists</h2>
|
||||
<ul>
|
||||
{% for list in info|keys %}
|
||||
<li>{{ list }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h2>Links</h2>
|
||||
<ul>
|
||||
{% if jiten_href is not null %}
|
||||
<li><a class="text-primary" href="{{ jiten_href }}">漢字辞典</a></li>
|
||||
{% else %}
|
||||
<li class="text-muted">漢字辞典</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a
|
||||
class="text-primary"
|
||||
href="{{ 'https://en.wiktionary.org/wiki/' ~ char|url_encode }}"
|
||||
>
|
||||
Wiktionary EN
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="text-primary"
|
||||
href="{{ 'https://ja.wiktionary.org/wiki/' ~ char|url_encode }}"
|
||||
>
|
||||
Wiktionary JA
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="text-primary"
|
||||
href="{{ 'https://forvo.com/word/' ~ char|url_encode ~ '/#zh' }}"
|
||||
>
|
||||
Forvo
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h2>Appeareances</h2>
|
||||
{% for refname, refcount in ref %}
|
||||
<div>{{ refname|basename }}: {{ refcount }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue