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\Entity\UnicodeNote;
|
||||||
use App\Service\AnkiService;
|
use App\Service\AnkiService;
|
||||||
use App\Service\CharListService;
|
use App\Service\CharListService;
|
||||||
|
use App\Utils\Japanese;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
@ -79,10 +80,6 @@ class KanjiController extends AbstractController
|
||||||
string $end,
|
string $end,
|
||||||
Request $request,
|
Request $request,
|
||||||
): Response {
|
): Response {
|
||||||
$jiten = json_decode(file_get_contents(
|
|
||||||
"{$this->getParameter('kernel.project_dir')}/data/kanken-links.json",
|
|
||||||
), true);
|
|
||||||
|
|
||||||
$charInfo = $this->getCharInfo(force: $request->isNoCache());
|
$charInfo = $this->getCharInfo(force: $request->isNoCache());
|
||||||
|
|
||||||
$chars = [];
|
$chars = [];
|
||||||
|
@ -115,4 +112,31 @@ class KanjiController extends AbstractController
|
||||||
|
|
||||||
return new Response();
|
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;
|
namespace App\Service;
|
||||||
|
|
||||||
use App\Utils\Japanese;
|
use App\Utils\Japanese;
|
||||||
|
use DirectoryIterator;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
class CharListService
|
class CharListService
|
||||||
{
|
{
|
||||||
|
@ -15,7 +17,7 @@ class CharListService
|
||||||
*
|
*
|
||||||
* @return array<string, string|array<string, 0>|array<string|array<string, 0>>>
|
* @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(
|
if (!ctype_alnum($name)) throw new \Exception(sprintf(
|
||||||
'Invalid name for list "%s". Only alphanumeric characters allowed',
|
'Invalid name for list "%s". Only alphanumeric characters allowed',
|
||||||
|
@ -68,6 +70,25 @@ class CharListService
|
||||||
return $ret;
|
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
|
* 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);
|
$value = hexdec($num);
|
||||||
return is_int($value) ? $value : null;
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block body %}{% endblock %}
|
{% 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -45,7 +45,12 @@
|
||||||
|
|
||||||
<div class="example-wrapper">
|
<div class="example-wrapper">
|
||||||
<h1 class="text-center p-2 mb-2">{{ block('title') }}</h1>
|
<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 %}
|
{% for char in characters %}
|
||||||
<div style="width: calc(100% / 16);">
|
<div style="width: calc(100% / 16);">
|
||||||
<div
|
<div
|
||||||
|
@ -56,8 +61,15 @@
|
||||||
"
|
"
|
||||||
title="{{ char.lists|keys|join("\n") }}"
|
title="{{ char.lists|keys|join("\n") }}"
|
||||||
>
|
>
|
||||||
<div class="kanji-card border-bottom fs-4 pb-1">
|
<div
|
||||||
{% if char.jiten_href != null %}
|
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
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="{{- char.jiten_href -}}"
|
href="{{- char.jiten_href -}}"
|
||||||
|
@ -65,14 +77,12 @@
|
||||||
>
|
>
|
||||||
{{- char.str -}}
|
{{- char.str -}}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}#}
|
||||||
{{- char.str -}}
|
{{- char.str -}}
|
||||||
{% endif %}
|
{#{% endif %}#}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="
|
<div style="padding-bottom: 2px;">
|
||||||
font-size: 0.5em;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
">
|
|
||||||
{% if char.lists.unicode is not defined %}
|
{% if char.lists.unicode is not defined %}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
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