Compare commits

...

10 Commits

10 changed files with 718 additions and 173 deletions

View File

@ -1,10 +1 @@
import './bootstrap.js';
/*
* Welcome to your app's main JavaScript file!
*
* This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig.
*/
import './styles/app.css';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

View File

@ -1,3 +0,0 @@
body {
background-color: skyblue;
}

View File

@ -0,0 +1,242 @@
<?php
namespace App\Command;
use App\Entity\SentenceListeningNote;
use App\Entity\SentenceNote;
use App\Entity\Term;
use App\Service\AnkiService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:create-production',
description: 'Add a short description for your command',
)]
class CreateProductionCommand extends Command
{
public function __construct(
private AnkiService $ankiService,
) {
parent::__construct();
}
private static function extractKanji(string $str): array
{
preg_match_all('/\p{Script=Han}/u', $str, $matches);
return array_unique($matches[0]);
}
private static function getOnlyKanji(string $str): string
{
return preg_replace('/[^\p{Script=Han}]/u', '', $str);
}
private static function kanjiDiff(array &$ref, string $subject): bool
{
$len = mb_strlen($subject);
$hasUnseenKanji = false;
for ($i = 0; $i < $len; $i++) {
$subKanji = mb_substr($subject, $i, 1);
foreach ($ref as $refKanji => $value) {
if ($subKanji === $refKanji) continue 2;
}
$ref[$subKanji] = 0;
$hasUnseenKanji = true;
}
return $hasUnseenKanji;
}
protected function configure(): void
{
//$this
// ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
// ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
//;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
printf('Getting all SentenceCards...');
$allIds = $this->ankiService->getAllSentenceNoteIds();
$allNotes = $this->ankiService->getNotes($allIds);
printf(" OK (%d)\n", count($allNotes));
printf('Getting all SentenceCards...');
$allListeningIds = $this->ankiService->getAllSentenceListeningNoteIds();
$allListeningNotes = $this->ankiService->getNotes($allListeningIds);
printf(" OK (%d)\n", count($allListeningNotes));
printf('Indexing all terms...');
$knownTerms = [];
$knownKanji = [];
$termCounts = [];
foreach ($allNotes as $note) {
if (!$note instanceof SentenceNote) throw new \Exception(sprintf(
'Expected SentenceNote, got %s',
$note::class,
));
foreach ($note->getTerms() as &$term) {
assert($term instanceof Term);
if (key_exists($term->getKanji(), $knownTerms)) continue;
$termCounts[$term->getKanji()] = 0;
$knownTerms[$term->getKanji()] = &$term;
foreach (self::extractKanji($term->getKanji()) as $kanji) {
$knownKanji[$kanji] = 0;
}
}
}
printf(" OK (%d)\n", count($knownTerms));
$total = count($knownTerms);
$i = 0;
foreach ($allNotes as $note) {
$i += 1;
if ($i % 12 === 0 or $i === $total) {
printf(
"\33[2K\r% 7d/% 7d | %.2f GiB | Getting frequencies",
$i,
$total,
memory_get_usage() / 1024 / 1024 / 1024
);
}
assert($note instanceof SentenceNote);
$sentKanji = str_replace(
"\u{200E}",
'',
strip_tags($note->getFields()['SentKanji'])
);
//foreach ($knownTerms as &$term) {
// assert($term instanceof Term);
// if (str_contains($sentKanji, $term->getKanji())) {
// $termCounts[$term->getKanji()] += 1;
// }
//}
foreach ($knownKanji as $kanji => &$count) {
if (str_contains($sentKanji, $kanji)) {
$count++;
}
}
}
printf("\n");
$seenKanji = [];
//uksort($knownTerms, function ($a, $b) {
// //return strlen(self::getOnlyKanji($b)) <=> strlen(self::getOnlyKanji($a)); // descending order
// return strlen($b) <=> strlen($a); // ascending order
//});
printf('Rating terms...');
foreach ($knownTerms as $term) {
$termKanji = self::getOnlyKanji($term->getKanji());
$weight = 1 / max(mb_strlen($termKanji), 1);
// First pass: Calculate the weight
foreach ($knownKanji as $kanji => $count) {
if (str_contains($termKanji, $kanji)) {
$termCounts[$term->getKanji()] += ceil($count * $weight);
}
}
}
arsort($termCounts);
// Have into account the ones that have already been created.
// This will not only skip them but take into account the kanjis they
// have.
foreach ($allListeningNotes as $listeningNote) {
assert($listeningNote instanceof SentenceListeningNote);
$termKanji = self::getOnlyKanji($listeningNote->getTerm()->getKanji());
self::kanjiDiff($seenKanji, $termKanji);
}
foreach ($termCounts as $term => $count) {
$termKanji = self::getOnlyKanji($term);
// Second pass: Penalize terms with no new kanji at all
if (!self::kanjiDiff($seenKanji, $termKanji)) {
unset($termCounts[$term]);
//unset($knownTerms[$term->getKanji()]);
//$termCounts[$term->getKanji()] = 0;
}
}
printf(" OK\n");
$theChosenTerm = null;
arsort($termCounts);
printf("\n");
foreach ($termCounts as $term => $count) {
$theChosenTerm = $knownTerms[$term];
$termKanji = self::getOnlyKanji($term);
printf("%s: %d\n", $term, $count);
$len = mb_strlen($termKanji);
for ($i = 0; $i < $len; $i++) {
$iKanji = mb_substr($termKanji, $i, 1);
printf(" - %s: %0.2f\n", $iKanji, $knownKanji[$iKanji] / $len);
}
printf("\n");
break;
};
$noteIds = $this->ankiService->findNotesIds(sprintf(
//'VocabKanji:*%s* "note:%s"',
'*%s* "note:%s"',
$theChosenTerm->getKanji(),
SentenceNote::MODEL_NAME,
));
$theChosenNote = $this->ankiService->getNote($noteIds[array_key_last($noteIds)]);
$newSlNote = SentenceListeningNote::fromNote($theChosenNote, $theChosenTerm);
if (!$this->ankiService->addNote($newSlNote, 'production')) {
throw new \Exception('Failed to add note!');
}
printf(
<<<FMNT
total: %d cards
max usage: %0.2f MiB
current usage: %0.2f MiB\n
FMNT,
count($termCounts),
memory_get_peak_usage() / 1024 / 1024,
memory_get_usage() / 1024 / 1024,
);
//dd($kanjiNotes);
//$io = new SymfonyStyle($input, $output);
//$arg1 = $input->getArgument('arg1');
//if ($arg1) {
// $io->note(sprintf('You passed an argument: %s', $arg1));
//}
//if ($input->getOption('option1')) {
// // ...
//}
//$io->success('You have a new command! Now make it your own! Pass --help to see your options.');
return Command::SUCCESS;
}
}

View File

@ -2,6 +2,9 @@
namespace App\Controller;
use App\Entity\Note;
use App\Entity\SentenceNote;
use App\Entity\Term;
use App\Service\AnkiService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@ -14,24 +17,67 @@ class AnkiController extends AbstractController
private AnkiService $ankiService,
) {}
public static function tmpl(string $path): string
{
return "anki/$path.html.twig";
}
#[Route('/', name: 'main')]
public function index(): Response
{
$note = $this->ankiService->getLatestNote();
$allIds = $this->ankiService->getAllSentenceNoteIds();
$allNotes = $this->ankiService->getNotes($allIds);
$this->ankiService->updateNote($note);
$kanjiNotes = [];
foreach ($allNotes as $note) {
if (!$note instanceof SentenceNote) throw new \Exception(sprintf(
'Expected SentenceNote, got %s',
$note::class,
));
dd($note);
dd($note->toAnki());
foreach ($note->getTerms() as $term) {
assert($term instanceof Term);
return $this->render('anki/index.html.twig', [
'controller_name' => $latestId,
if (key_exists($term->getKanji(), $kanjiNotes)) continue;
$newNote = new Note();
echo $note->getFields()['SentKanji'];
echo '<br><small>';
echo var_dump($note->getHighlightedKanji());
echo '</small><br><br>';
//echo $term->getKanji();
//echo ' | ';
$kanjiNotes[$term->getKanji()] = $newNote;
}
}
die();
}
#[Route('/nonconforming', name: 'nonconforming')]
public function nonconforming(): Response
{
$allIds = $this->ankiService->getAllNoteIds();
$allNotes = $this->ankiService->getNotes($allIds);
$lacking = array_filter($allNotes, function ($note) {
assert($note instanceof Note);
return empty($note->getTerms());
});
return $this->render(self::tmpl('nonconforming'), [
'notes' => $lacking,
]);
}
#[Route('/note/{nid}/get', name: 'get_note')]
public function get_note(int $nid)
{
dd($this->ankiService->getNote($nid));
$note = $this->ankiService->getNote($nid);
//$this->ankiService->updateNote($note);
dd($note);
}
}

View File

@ -2,50 +2,57 @@
namespace App\Entity;
//use App\Repository\NoteRepository;
//use Doctrine\ORM\Mapping as ORM;
//#[ORM\Entity(repositoryClass: NoteRepository::class)]
class Note
{
//#[ORM\Id]
//#[ORM\GeneratedValue]
//#[ORM\Column]
private int $id;
private int $mod;
private array $terms = [];
private string $profile;
private array $tags = [];
private string $model;
// Maybe these doesn't make sense to keep but leaving it here just in
// case for handiness' sake
private array $fields = [];
private ?array $mediaInfo = null;
private array $cardIds;
const HIGHLIGHT_PATTERN = '/<span\s+([^>]*)>(.*?)<\/span>/i';
const HIGHLIGHT_ATTR_KANJI = 'style="color: rgb(255, 78, 8);"';
protected ?int $id;
protected ?int $mod;
protected string $model;
protected string $profile;
protected array $cardIds = [];
protected array $fields = [];
protected array $tags = [];
// -------------------------------------------------- Getters & setters ---
public function getId(): int
{
return $this->id;
}
public function hasTerm(string $kanji): bool
public function setId(int $id): static
{
foreach ($this->terms as $term) {
assert($term instanceof Term);
if ($term->kanji == $kanji) return true;
}
return false;
$this->id = $id;
return $this;
}
public function getModel(): string
{
return $this->model;
}
public static function fromAnki(array $noteInfo): self
public function getFields(): array
{
$note = new self();
return $this->fields;
}
public function setFields(array $fields): static
{
$this->fields = $fields;
return $this;
}
/** @return list<string> */
public function getTags(): array
{
return $this->tags;
}
// ------------------------------------------------------- Anki-related ---
public static function fromAnki(array $noteInfo): static
{
$note = new static();
[
'noteId' => $note->id,
@ -64,49 +71,6 @@ class Note
// the order would be advisable.
$note->fields = array_map(fn($x) => $x['value'], $noteInfo['fields']);
$note->mediaInfo = $note->parseMediaInfo($note->fields['Notes']);
// Set VocabKanji field
$terms = Term::fromNoteFields($note->fields);
// If not defined, find them from the highlighted parts in the sentence
if (empty($terms)) {
// 1. Get all spans in the text
preg_match_all(
self::HIGHLIGHT_PATTERN,
$note->fields['SentKanji'],
$matches,
PREG_SET_ORDER,
);
// 2. Check the ones that match with the kanji color
foreach ($matches as $match) {
if ($match[1] === self::HIGHLIGHT_ATTR_KANJI) {
$term = new Term();
$term->kanji = mb_trim($match[2]);
$term->definitionEn = null;
$term->definitionJp = null;
$note->terms[] = $term;
}
}
}
if (empty($terms)) dd($note);
// Set to null whatever is null
$readings = array_map(
fn($x) => in_array($x, ['_', '_', '']) ? null : $x,
explode('', $note->fields['VocabFurigana']),
);
// Set readings from furigana field
foreach ($note->terms as $key => &$term) {
if (null === $term->getReading()) {
if (null !== ($readings[$key] ?? null)) {
$term->kanji .= '[' . $readings[$key] . ']';
}
}
}
return $note;
}
@ -114,65 +78,11 @@ class Note
{
return [
'id' => $this->id,
'fields' => [
'VocabKanji' => join('', array_map(
fn(Term $x) => $x->getKanji(),
$this->terms,
)),
'VocabFurigana' => join('', array_map(
fn(Term $x) => $x->getReading() ?? '_',
$this->terms,
)),
'VocabDef' => join("<br>\n", array_map(
fn(Term $x) => $x->toAnkiVocabDef(),
$this->terms,
)),
],
];
}
public function parseMediaInfo(string $notes): ?array
{
$matches = null;
// Parse the notes fields. It can be in the form of
// series-name_S01 EP07 (11h22m33s44ms)
// or
// movie-name EP (11h22m33s44ms)
if (1 !== preg_match(
'/(?<name>[a-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
$notes,
$matches,
)) {
return null;
}
// Remove number-indexed matches, cast numbers to integers
$matches = [
'name' => $matches['name'],
'time' => $matches['time'],
// NOTE: intval returns 0 if not a number, which is false-like
'season' => intval($matches['season']) ?: null,
'episode' => intval($matches['episode']) ?: null,
];
// Parse time into a DateInterval and replace it in the matches array
$time = new \DateInterval('PT0S');
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
preg_match('/(\d+)s/', $matches['time'], $seconds);
preg_match('/(\d+)m/', $matches['time'], $minutes);
preg_match('/(\d+)h/', $matches['time'], $hours);
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
if ($seconds[1] ?? false) $time->s = $seconds[1];
if ($minutes[1] ?? false) $time->i = $minutes[1];
if ($hours[1] ?? false) $time->h = $hours[1];
$matches['time'] = $time;
return $matches;
}
// ---------------------------------------------------- Derived methods ---
public function getCreatedAt(): \DateTimeImmutable
{

View File

@ -0,0 +1,126 @@
<?php
namespace App\Entity;
class SentenceListeningNote extends Note
{
const MODEL_NAME = 'Japanese sentences listening';
private ?array $mediaInfo = [];
private ?Term $term = null;
// -------------------------------------------------- Getters & setters ---
public function getTerm(): Term
{
return $this->term;
}
public function setTerm(Term $term): static
{
$this->fields['VocabKanji'] = $term->getKanji();
$this->fields['VocabFurigana'] = $term->getReading();
$this->fields['VocabDef'] = $term->toAnkiVocabDef();
$this->term = $term;
return $this;
}
// ------------------------------------------------------- Anki-related ---
public static function fromAnki(array $noteInfo): static
{
$note = parent::fromAnki($noteInfo);
if ($note->getModel() !== self::MODEL_NAME) {
throw new \Exception('Trying to parse wrong model');
}
$note->mediaInfo = self::parseMediaInfo($note->fields['Notes']);
// Set VocabKanji field
$note->term = Term::fromNoteFields($note->fields)[0] ?? null;
if ($note->term === null) {
throw new \Exception("Couldn't get term for Listening card");
}
return $note;
}
public static function fromNote(Note $origNote, Term $term): static
{
$slNote = new static();
foreach (get_object_vars($origNote) as $prop => $value) {
$slNote->$prop = $value;
}
// Related fields are updated using the setter
$slNote->setTerm($term);
// Remove highlighting
$slNote->fields['SentKanji'] = strip_tags($slNote->fields['SentKanji']);
// Reset relations and basic data
$slNote->id = null;
$slNote->model = self::MODEL_NAME;
$slNote->cardIds = [];
return $slNote;
}
public function toAnki(): array
{
return $this->fields;
}
// ---------------------------------------------------- Derived methods ---
public function isSentKanjiHighlighted(): bool
{
return str_contains(
$this->fields['SentKanji'],
self::HIGHLIGHT_ATTR_KANJI,
);
}
private static function parseMediaInfo(string $notes): ?array
{
$matches = null;
// Parse the notes fields. It can be in the form of
// series-name_S01 EP07 (11h22m33s44ms)
// or
// movie-name EP (11h22m33s44ms)
if (1 !== preg_match(
'/(?<name>[0-9A-Za-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
$notes,
$matches,
)) {
return null;
}
// Remove number-indexed matches, cast numbers to integers
$matches = [
'name' => $matches['name'],
'time' => $matches['time'],
// NOTE: intval returns 0 if not a number, which is false-like
'season' => intval($matches['season']) ?: null,
'episode' => intval($matches['episode']) ?: null,
];
// Parse time into a DateInterval and replace it in the matches array
$time = new \DateInterval('PT0S');
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
preg_match('/(\d+)s/', $matches['time'], $seconds);
preg_match('/(\d+)m/', $matches['time'], $minutes);
preg_match('/(\d+)h/', $matches['time'], $hours);
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
if ($seconds[1] ?? false) $time->s = $seconds[1];
if ($minutes[1] ?? false) $time->i = $minutes[1];
if ($hours[1] ?? false) $time->h = $hours[1];
$matches['time'] = $time;
return $matches;
}
}

177
src/Entity/SentenceNote.php Normal file
View File

@ -0,0 +1,177 @@
<?php
namespace App\Entity;
class SentenceNote extends Note
{
const MODEL_NAME = 'Japanese sentences';
private ?array $mediaInfo = [];
private array $terms = [];
// -------------------------------------------------- Getters & setters ---
public function getTerms(): array
{
return $this->terms;
}
public function setTerms(array $terms): static
{
$this->terms = $terms;
return $this;
}
// ------------------------------------------------------- Anki-related ---
public static function fromAnki(array $noteInfo): static
{
$note = parent::fromAnki($noteInfo);
if ($note->getModel() !== self::MODEL_NAME) {
throw new \Exception('Trying to parse wrong model');
}
$note->mediaInfo = self::parseMediaInfo($note->fields['Notes']);
// Set VocabKanji field
$note->terms = Term::fromNoteFields($note->fields);
// If unable to, create them from the highlighted parts in the sentence
if (empty($note->terms)) {
foreach ($note->getHighlightedKanji() as $highlighedKanji) {
$term = new Term();
$term->kanji = $highlighedKanji;
$term->definitionEn = null;
$term->definitionJp = null;
$note->terms[] = $term;
}
}
// Set to null whatever is null
$readings = array_map(
fn($x) => in_array($x, ['_', '_', '']) ? null : $x,
explode('', $note->fields['VocabFurigana']),
);
// Set readings from furigana field
foreach ($note->terms as $key => &$term) {
if (null === $term->getReading()) {
if (null !== ($readings[$key] ?? null)) {
$term->kanji .= '[' . $readings[$key] . ']';
}
}
}
return $note;
}
public function toAnki(): array
{
return array_merge(parent::toAnki(), [
'fields' => [
'VocabKanji' => join('', array_map(
fn(Term $x) => $x->getKanji(),
$this->terms,
)),
'VocabFurigana' => join('', array_map(
fn(Term $x) => $x->getReading() ?? '_',
$this->terms,
)),
'VocabDef' => join("<br>\n", array_map(
fn(Term $x) => $x->toAnkiVocabDef(),
$this->terms,
)),
],
]);
}
// ---------------------------------------------------- Derived methods ---
public function hasTerm(string $kanji): bool
{
foreach ($this->terms as $term) {
assert($term instanceof Term);
if ($term->kanji == $kanji) return true;
}
return false;
}
public function isSentKanjiHighlighted(): bool
{
return str_contains(
$this->fields['SentKanji'],
self::HIGHLIGHT_ATTR_KANJI,
);
}
/** Return an array of strings with the highlighted kanji in the SentKanji */
public function getHighlightedKanji(): array
{
$ret = [];
$matches = [];
// 1. Get all spans in the text
preg_match_all(
self::HIGHLIGHT_PATTERN,
$this->fields['SentKanji'],
$matches,
PREG_SET_ORDER,
);
// 2. Check the ones that match with the kanji color
foreach ($matches as $match) {
if ($match[1] === self::HIGHLIGHT_ATTR_KANJI) {
$ret[] = mb_trim($match[2]);
}
}
return $ret;
}
private static function parseMediaInfo(string $notes): ?array
{
$matches = null;
// Parse the notes fields. It can be in the form of
// series-name_S01 EP07 (11h22m33s44ms)
// or
// movie-name EP (11h22m33s44ms)
if (1 !== preg_match(
'/(?<name>[0-9A-Za-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
$notes,
$matches,
)) {
return null;
}
// Remove number-indexed matches, cast numbers to integers
$matches = [
'name' => $matches['name'],
'time' => $matches['time'],
// NOTE: intval returns 0 if not a number, which is false-like
'season' => intval($matches['season']) ?: null,
'episode' => intval($matches['episode']) ?: null,
];
// Parse time into a DateInterval and replace it in the matches array
$time = new \DateInterval('PT0S');
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
preg_match('/(\d+)s/', $matches['time'], $seconds);
preg_match('/(\d+)m/', $matches['time'], $minutes);
preg_match('/(\d+)h/', $matches['time'], $hours);
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
if ($seconds[1] ?? false) $time->s = $seconds[1];
if ($minutes[1] ?? false) $time->i = $minutes[1];
if ($hours[1] ?? false) $time->h = $hours[1];
$matches['time'] = $time;
return $matches;
}
}

View File

@ -3,6 +3,8 @@
namespace App\Service;
use App\Entity\Note;
use App\Entity\SentenceListeningNote;
use App\Entity\SentenceNote;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AnkiService
@ -30,7 +32,38 @@ class AnkiService
return $result;
}
public function getAllNoteIds(): array
/** The note's id is updated on success.
* @return bool True on success
*/
public function addNote(Note &$note, string $deckName): bool
{
$note->setId($this->request('addNote', ['note' => [
'deckName' => $deckName,
'modelName' => $note->getModel(),
'fields' => $note->getFields(),
'options' => ['allowDuplicate' => false],
'tags' => array_merge($note->getTags(), ['anker_made']),
]]));
return $note->getId() !== null;
}
public function findNotesIds(string $query): array
{
return $this->request('findNotes', ['query' => $query]);
}
public function getAllSentenceListeningNoteIds(): array
{
$query = sprintf(
'"note:%s" -is:suspended',
SentenceListeningNote::MODEL_NAME,
);
return $this->request('findNotes', ['query' => $query]);
}
public function getAllSentenceNoteIds(): array
{
return $this->request(
'findNotes',
@ -41,7 +74,7 @@ class AnkiService
/** Give an array of IDs, the note Infos are returned. if info for a given
* doesn't exist, it is assigned to null instead of the default [].
*/
private function getNotesInfo(array $noteIds): array
public function getNotesInfo(array $noteIds): array
{
$noteInfos = $this->request('notesInfo', ['notes' => $noteIds]);
@ -53,21 +86,42 @@ class AnkiService
}
/** Returns info form note given an ID, returns null if it doesn't exist */
private function getNoteInfo(int $noteId): ?array
public function getNoteInfo(int $noteId): ?array
{
return $this->getNotesInfo([$noteId])[0];
}
public function getNotes(array $nids): array
{
return array_map(self::parseNoteInfo(...), $this->getNotesInfo($nids));
}
public function getNote(int $nid): ?Note
{
return Note::fromAnki($this->getNoteInfo($nid));
$noteInfo = $this->getNoteInfo($nid)
?? throw new \Exception("Note $nid not found.");
return self::parseNoteInfo($noteInfo);
}
private static function parseNoteInfo(array $noteInfo): Note
{
return match ($noteInfo['modelName']) {
SentenceNote::MODEL_NAME => SentenceNote::fromAnki($noteInfo),
SentenceListeningNote::MODEL_NAME => SentenceListeningNote::fromAnki($noteInfo),
default => throw new \Exception(sprintf(
'Unrecognized Note "%s" of type "%s"',
$noteInfo['noteId'],
$noteInfo['modelName'],
))
};
}
public function getLatestNote(): ?Note
{
// NoteIDs are just timestamps in milliseconds, so the latest is just
// the biggest numerically
$latestId = max($this->getAllNoteIds());
$latestId = max($this->getAllSentenceNoteIds());
return $this->getNote($latestId);
}

View File

@ -1,20 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Hello AnkiController!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code>/strg/prgm/web/anker/src/Controller/AnkiController.php</code></li>
<li>Your template at <code>/strg/prgm/web/anker/templates/anki/index.html.twig</code></li>
</ul>
</div>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends 'base.html.twig' %}
{% block title %}Main{% endblock %}
{% block body %}
<div class="example-wrapper">
<h1>hello</h1>
<span>total {{ notes|length }}</span>
{% for note in notes %}
<div style="
border: 1px solid black;
margin-bottom: 10px;
padding: 5px;
border-radius: 5px;
">
{{ note.fields.SentKanji }}
<br>
<a href="{{ path('app_anki_get_note', {nid: note.id}) }}">view</a>
</div>
{% endfor %}
</div>
{% endblock %}