diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ccd231d..00d02864b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,138 @@ ones are marked like "v1.0.0-fork". ## [Unreleased] +### Removed + +* **The last of the frame-era result plumbing** (#266, #262): four result views + still emitted a ` +
diff --git a/src/Modules/Book/Application/Services/EpubParserService.php b/src/Modules/Book/Application/Services/EpubParserService.php index 9d98a2ba9..f1dc3a44a 100644 --- a/src/Modules/Book/Application/Services/EpubParserService.php +++ b/src/Modules/Book/Application/Services/EpubParserService.php @@ -17,19 +17,20 @@ namespace Lwt\Modules\Book\Application\Services; -use Kiwilan\Ebook\Ebook; -use Kiwilan\Ebook\Formats\Epub\EpubModule; -use Kiwilan\Ebook\Formats\Epub\Parser\EpubChapter; -use Kiwilan\Ebook\Formats\Epub\Parser\EpubHtml; -use Kiwilan\Ebook\Models\BookAuthor; +use Lwt\Modules\Book\Infrastructure\Epub\EpubBook; +use Lwt\Modules\Book\Infrastructure\Epub\EpubChapter; +use Lwt\Modules\Book\Infrastructure\Epub\EpubDocument; +use Lwt\Modules\Book\Infrastructure\Epub\EpubReader; use InvalidArgumentException; use RuntimeException; /** * Service for parsing EPUB files and extracting content. * - * Uses the kiwilan/php-ebook library to read EPUB files and extract - * metadata and chapter content for import into LWT. + * Reads EPUB files through the in-tree {@see EpubReader} and extracts metadata + * and chapter content for import into LWT. LWT bundled its own reader in 3.4.0 + * so that EPUB import, a core feature, no longer depends on an external + * Composer package (#263). * * @since 3.0.0 */ @@ -110,22 +111,14 @@ public function parse(string $filePath, string $originalName = ''): array $this->assertZipWithinLimits($filePath); try { - $ebook = Ebook::read($filePath, $this->resolveFormat($filePath, $originalName)); - if ($ebook === null) { - throw new RuntimeException( - "Failed to read EPUB file: {$filePath}. " - . "The file may be corrupted or not a valid EPUB format." - ); - } + $ebook = EpubReader::read($filePath); } catch (\Throwable $e) { - // Provide more specific error messages - $message = $e->getMessage(); - if (str_contains($message, 'getManifest() on null')) { - $message = "EPUB file appears to be corrupted or has an invalid internal structure (missing manifest)."; - } elseif (str_contains($message, 'ZIP')) { - $message = "EPUB file could not be read as a ZIP archive. The file may be corrupted."; - } - throw new RuntimeException("Failed to parse EPUB file: {$message}", 0, $e); + $label = $originalName !== '' ? $originalName : $filePath; + throw new RuntimeException( + "Failed to parse EPUB file '{$label}': " . $e->getMessage(), + 0, + $e + ); } $metadata = [ @@ -147,64 +140,44 @@ public function parse(string $filePath, string $originalName = ''): array /** * Extract the primary author name from an ebook. * - * @param Ebook $ebook The ebook object + * @param EpubBook $ebook The parsed ebook * * @return string|null Author name or null if not found */ - private function extractAuthor(Ebook $ebook): ?string + private function extractAuthor(EpubBook $ebook): ?string { - $author = $ebook->getAuthorMain(); - if ($author !== null) { - return $author->getName(); - } - - /** @var BookAuthor[] $authors */ - $authors = $ebook->getAuthors(); - if (!empty($authors)) { - return $authors[0]->getName(); - } - - return null; + return $ebook->getAuthorMain(); } /** * Extract chapters from an ebook. * - * @param Ebook $ebook The ebook object + * @param EpubBook $ebook The parsed ebook * * @return array */ - private function extractChapters(Ebook $ebook): array + private function extractChapters(EpubBook $ebook): array { $chapters = []; $chapterNum = 1; - // Try to get chapters from the ebook via the EPUB parser - $epubModule = $this->getEpubModule($ebook); - if ($epubModule !== null) { - try { - /** @var EpubChapter[] $ebookChapters */ - $ebookChapters = $epubModule->getChapters(); - - foreach ($ebookChapters as $chapter) { - $content = $this->cleanHtmlContent($chapter->getContent()); - - // Skip empty chapters - if (trim($content) === '') { - continue; - } - - $chapters[] = [ - 'num' => $chapterNum, - 'title' => $chapter->getLabel() ?: "Chapter {$chapterNum}", - 'content' => $content, - ]; - $chapterNum++; - } - } catch (\Throwable $e) { - // If chapter extraction fails, log the error and continue with HTML fallback - error_log("EPUB chapter extraction failed, trying HTML fallback: " . $e->getMessage()); + /** @var EpubChapter[] $ebookChapters */ + $ebookChapters = $ebook->getChapters(); + + foreach ($ebookChapters as $chapter) { + $content = $this->cleanHtmlContent($chapter->getContent()); + + // Skip empty chapters + if (trim($content) === '') { + continue; } + + $chapters[] = [ + 'num' => $chapterNum, + 'title' => $chapter->getLabel() ?: "Chapter {$chapterNum}", + 'content' => $content, + ]; + $chapterNum++; } // If no chapters found, try to extract from HTML files @@ -215,64 +188,38 @@ private function extractChapters(Ebook $ebook): array return $chapters; } - /** - * Get the EpubModule from an Ebook. - * - * @param Ebook $ebook The ebook object - * - * @return EpubModule|null The EPUB module or null if not an EPUB - */ - private function getEpubModule(Ebook $ebook): ?EpubModule - { - $parser = $ebook->getParser(); - if ($parser === null) { - return null; - } - return $parser->getEpub(); - } - /** * Extract content from HTML files in the EPUB as fallback. * - * @param Ebook $ebook The ebook object + * @param EpubBook $ebook The parsed ebook * * @return array */ - private function extractFromHtmlFiles(Ebook $ebook): array + private function extractFromHtmlFiles(EpubBook $ebook): array { $chapters = []; $chapterNum = 1; - // Try to get HTML content via the EPUB module - $epubModule = $this->getEpubModule($ebook); - if ($epubModule !== null) { - try { - /** @var EpubHtml[] $htmlFiles */ - $htmlFiles = $epubModule->getHtml(); - foreach ($htmlFiles as $htmlFile) { - if ($this->isNavigationFile($htmlFile)) { - continue; - } - - $content = $this->cleanHtmlContent($htmlFile->getBody() ?? ''); - - if (trim($content) === '') { - continue; - } - - // Try to extract title from content - $title = $this->extractTitleFromContent($content, $chapterNum); - - $chapters[] = [ - 'num' => $chapterNum, - 'title' => $title, - 'content' => $content, - ]; - $chapterNum++; - } - } catch (\Throwable $e) { - error_log("EPUB HTML extraction fallback failed: " . $e->getMessage()); + foreach ($ebook->getHtml() as $htmlFile) { + if ($this->isNavigationFile($htmlFile)) { + continue; + } + + $content = $this->cleanHtmlContent($htmlFile->getBody()); + + if (trim($content) === '') { + continue; } + + // Try to extract title from content + $title = $this->extractTitleFromContent($content, $chapterNum); + + $chapters[] = [ + 'num' => $chapterNum, + 'title' => $title, + 'content' => $content, + ]; + $chapterNum++; } return $chapters; @@ -282,14 +229,14 @@ private function extractFromHtmlFiles(Ebook $ebook): array * Detect EPUB 3 navigation / TOC documents that should not appear as * chapters. * - * The kiwilan library's NCX-based getChapters() ignores nav.xhtml, but - * when an EPUB ships without an NCX the HTML fallback would otherwise - * include the nav document as a phantom chapter. Filename heuristics - * cover the common cases (nav.xhtml, toc.xhtml); the body sniff catches - * less conventionally-named EPUB 3 nav documents identified by the + * The TOC-driven getChapters() path ignores nav.xhtml, but when an EPUB + * ships without a usable table of contents the HTML fallback would + * otherwise include the nav document as a phantom chapter. Filename + * heuristics cover the common cases (nav.xhtml, toc.xhtml); the body sniff + * catches less conventionally-named EPUB 3 nav documents identified by the * `epub:type="toc"` (or related) attribute on a `
-
> +
+ + + + +>
@@ -135,7 +168,6 @@ __('common.required_field')]); ?> -

@@ -197,10 +229,14 @@ class="input"

+ + diff --git a/src/Modules/Book/Views/import_result.php b/src/Modules/Book/Views/import_result.php deleted file mode 100644 index 63bfe2551..000000000 --- a/src/Modules/Book/Views/import_result.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - */ - -declare(strict_types=1); - -namespace Lwt\Views\Book; - -use Lwt\Shared\UI\Helpers\IconHelper; -use Lwt\Shared\UI\Helpers\PageLayoutHelper; - -?> - -

- -
- -
- -
- - - __('book.view_book')]); ?> - - - - - - __('book.import_another_epub')]); ?> - - - - - __('book.all_books')]); ?> - - -
diff --git a/src/Modules/Dictionary/Http/TranslationController.php b/src/Modules/Dictionary/Http/TranslationController.php index 5344d0e57..b1a990e5c 100644 --- a/src/Modules/Dictionary/Http/TranslationController.php +++ b/src/Modules/Dictionary/Http/TranslationController.php @@ -232,7 +232,7 @@ protected function renderTermTranslation( 'text' => $text, 'langId' => $lgId, 'hasParentFrame' => $hasParentFrame - ]); ?> + ], JSON_HEX_TAG | JSON_HEX_AMP); ?> param('lang') !== '' ? (int) $this->param('lang') : null; - $textId = $this->param('text') !== '' ? (int) $this->param('text') : null; - $selection = $this->param('selection') !== '' ? (int) $this->param('selection') : null; - - // Get selection data from session criteria - $sessReviewSql = null; - if ($selection !== null && $this->sessionManager->hasCriteria()) { - $sessReviewSql = $this->sessionManager->getSelectionString(); - } - - // Get review SQL - $identifier = $this->reviewFacade->getReviewIdentifier( - $selection, - $sessReviewSql, - $langId, - $textId - ); - - if ($identifier[0] === '') { - throw ValidationException::forField( - 'parameters', - 'Review table requires valid lang, text, or selection parameter' - )->setHttpStatusCode(400); - } - - /** @psalm-suppress InvalidScalarArgument */ - $reviewResult = $this->reviewFacade->getReviewSql($identifier[0], $identifier[1]); - - if ($reviewResult === null) { - echo '

Sorry - Unable to generate review SQL

'; - return; - } - - $reviewsql = $reviewResult['sql']; - $reviewParams = $reviewResult['params']; - - // Validate single language - $validation = $this->reviewFacade->validateReviewSelection($reviewsql, $reviewParams); - if (!$validation['valid']) { - echo '

Sorry - ' . ($validation['error'] ?? 'Unknown error') . '

'; - return; - } - - // Get language settings - $langIdFromSql = $this->reviewFacade->getLanguageIdFromReviewSql($reviewsql, $reviewParams); - if ($langIdFromSql === null) { - include __DIR__ . '/../Views/no_terms.php'; - PageLayoutHelper::renderPageEnd(); - return; - } - - $langSettings = $this->reviewFacade->getLanguageSettings($langIdFromSql); - $textSizeRaw = isset($langSettings['textSize']) ? (int) $langSettings['textSize'] : 100; - $textSize = (int) round(($textSizeRaw - 100) / 2, 0) + 100; - - // Render table settings - $settings = $this->reviewFacade->getTableReviewSettings(); - include __DIR__ . '/../Views/table_review_settings.php'; - - echo ''; - include __DIR__ . '/../Views/table_review_header.php'; - - // Render table rows - $wordsArray = $this->reviewFacade->getTableReviewWords($reviewsql, $reviewParams); - /** @var mixed $regexWordRaw */ - $regexWordRaw = $langSettings['regexWord'] ?? ''; - $regexWord = is_string($regexWordRaw) ? $regexWordRaw : ''; - $rtl = (bool) ($langSettings['rtl'] ?? false); - - foreach ($wordsArray as $word) { - include __DIR__ . '/../Views/table_review_row.php'; - } - - echo '
'; - } - /** * Get review property from request parameters. * diff --git a/src/Modules/Review/Views/table_review_header.php b/src/Modules/Review/Views/table_review_header.php deleted file mode 100644 index e77ed85c6..000000000 --- a/src/Modules/Review/Views/table_review_header.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - */ - -declare(strict_types=1); - -namespace Lwt\Views\Review; - -?> - - - - - - - - - - - - - - - - - - diff --git a/src/Modules/Review/Views/table_review_row.php b/src/Modules/Review/Views/table_review_row.php deleted file mode 100644 index e4673b127..000000000 --- a/src/Modules/Review/Views/table_review_row.php +++ /dev/null @@ -1,108 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - * - * @var array $word - * @var string $regexWord - * @var int $textSize - * @var bool $rtl - */ - -namespace Lwt\Views\Review; - -use Lwt\Shared\Infrastructure\Utilities\StringUtils; -use Lwt\Modules\Vocabulary\Application\Services\ExportService; -use Lwt\Modules\Vocabulary\Application\Helpers\StatusHelper; -use Lwt\Shared\UI\Helpers\IconHelper; - -// Validate and cast injected variables -assert(isset($word) && is_array($word)); -assert(isset($regexWord) && is_string($regexWord)); -assert(isset($textSize) && is_int($textSize)); -assert(isset($rtl) && is_bool($rtl)); - -$isRtl = $rtl; -$span1 = $isRtl ? '' : ''; -$span2 = $isRtl ? '' : ''; - -// Extract typed values from word array -$woId = (int) ($word['WoID'] ?? 0); -$woText = (string) ($word['WoText'] ?? ''); -$woTranslation = (string) ($word['WoTranslation'] ?? ''); -$woRomanization = (string) ($word['WoRomanization'] ?? ''); -$woSentence = (string) ($word['WoSentence'] ?? ''); -$woStatus = (int) ($word['WoStatus'] ?? 0); -$woScore = (int) ($word['Score'] ?? 0); - -$sent = htmlspecialchars(ExportService::replaceTabNewline($woSentence), ENT_QUOTES, 'UTF-8'); -$sent1 = str_replace( - "{", - ' [', - str_replace( - "}", - '] ', - ExportService::maskTermInSentence($sent, $regexWord) - ) -); -?> - - - - - $editTermLabel, 'alt' => $editTermLabel] - ); ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Modules/Review/Views/table_review_settings.php b/src/Modules/Review/Views/table_review_settings.php deleted file mode 100644 index 245c7e9ec..000000000 --- a/src/Modules/Review/Views/table_review_settings.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - * - * @var array{edit: int, status: int, term: int, trans: int, rom: int, sentence: int} $settings - */ - -namespace Lwt\Views\Review; - -use Lwt\Shared\UI\Helpers\FormHelper; - -/** @var array{edit: int, status: int, term: int, trans: int, rom: int, sentence: int} $settings */ - -?> -

- /> - - /> - - /> - - /> - - /> - - /> - -

diff --git a/src/Modules/Text/Views/edit_form.php b/src/Modules/Text/Views/edit_form.php index 13ad3587b..9ad66343b 100644 --- a/src/Modules/Text/Views/edit_form.php +++ b/src/Modules/Text/Views/edit_form.php @@ -108,6 +108,7 @@ x-data="textNewForm" :action="formAction()" + @submit="handleSubmit($event)" @webpage-imported="goToReview()" x-data @@ -963,6 +964,11 @@ class="textarea notempty checkoutsidebmp" + +
+ +
+
diff --git a/src/Modules/Vocabulary/Application/Services/ExpressionService.php b/src/Modules/Vocabulary/Application/Services/ExpressionService.php index cbd71872b..a2d99d0b3 100644 --- a/src/Modules/Vocabulary/Application/Services/ExpressionService.php +++ b/src/Modules/Vocabulary/Application/Services/ExpressionService.php @@ -307,7 +307,9 @@ public function insertExpressions(string $textlc, int $lid, int $wid, int $len, $txId = $occ['SeTxID'] ?? $occ['TxID'] ?? 0; $appendtext[$txId] = []; if (Settings::getZeroOrOne('showallwords', 1)) { - $appendtext[$txId][$occ['position']] = " $len "; + // A literal non-breaking space, not the entity: the client + // sets this as text content, so markup would show verbatim. + $appendtext[$txId][$occ['position']] = "\u{00A0}$len\u{00A0}"; } else { if ('MECAB' == strtoupper(trim($regexp))) { $appendtext[$txId][$occ['position']] = $occ['term'] ?? ''; @@ -384,7 +386,7 @@ public function newMultiWordInteractable(string $hex, array $multiwords, int $wi 'multiWords' => $multiwords, 'hex' => $hex, 'showAll' => $showAll - ]); ?> + ], JSON_HEX_TAG | JSON_HEX_AMP); ?> $len, 'hex' => $hex, 'showAll' => $showAll - ]); ?> + ], JSON_HEX_TAG | JSON_HEX_AMP); ?> $wordId, - "text" => $text, - "romanization" => $roman, - "translation" => $translation, - "status" => $status - ]; - - $json = json_encode($data); - if ($json === false) { - $json = json_encode(["error" => "Unable to return data."]); - if ($json === false) { - throw new \RuntimeException("Unable to return data"); - } - } - return $json; - } } diff --git a/src/Modules/Vocabulary/Application/UseCases/CreateTermFromHover.php b/src/Modules/Vocabulary/Application/UseCases/CreateTermFromHover.php deleted file mode 100644 index e38af22fd..000000000 --- a/src/Modules/Vocabulary/Application/UseCases/CreateTermFromHover.php +++ /dev/null @@ -1,154 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - */ - -declare(strict_types=1); - -namespace Lwt\Modules\Vocabulary\Application\UseCases; - -use Lwt\Modules\Dictionary\Application\DictionaryFacade; -use Lwt\Modules\Dictionary\Application\Services\LocalDictionaryService; -use Lwt\Shared\Infrastructure\Utilities\StringUtils; -use Lwt\Shared\Infrastructure\Database\Connection; -use Lwt\Shared\Infrastructure\Database\Escaping; -use Lwt\Shared\Infrastructure\Database\UserScopedQuery; -use Lwt\Modules\Vocabulary\Application\VocabularyFacade; - -/** - * Use case for creating a term from the text reading hover action. - * - * When a user clicks on a word status in the reading view hover menu, - * this use case creates the term with the specified status. - * - * @since 3.0.0 - */ -class CreateTermFromHover -{ - private VocabularyFacade $vocabularyFacade; - private DictionaryFacade $dictionaryFacade; - - /** - * Constructor. - * - * @param VocabularyFacade|null $vocabularyFacade Vocabulary facade - * @param DictionaryFacade|null $dictionaryFacade Dictionary facade - */ - public function __construct( - ?VocabularyFacade $vocabularyFacade = null, - ?DictionaryFacade $dictionaryFacade = null - ) { - $this->vocabularyFacade = $vocabularyFacade ?? new VocabularyFacade(); - $this->dictionaryFacade = $dictionaryFacade - ?? new DictionaryFacade(new LocalDictionaryService()); - } - - /** - * Execute the use case. - * - * @param int $textId Text ID - * @param string $wordText Word text to create - * @param int $status Word status (1-5) - * @param string $sourceLang Source language code (for translation) - * @param string $targetLang Target language code (for translation) - * - * @return array{ - * wid: int, - * word: string, - * wordRaw: string, - * translation: string, - * status: int, - * hex: string - * } - */ - public function execute( - int $textId, - string $wordText, - int $status, - string $sourceLang = '', - string $targetLang = '' - ): array { - // Get translation if status is 1 (new word) and translation params provided - $translation = '*'; - if ($status === 1 && $sourceLang !== '' && $targetLang !== '') { - $translationResult = $this->dictionaryFacade->translate( - $wordText, - $sourceLang, - $targetLang - ); - if ($translationResult !== false && isset($translationResult[0])) { - $translation = $translationResult[0]; - } - // Don't use word as its own translation - if ($translation === $wordText) { - $translation = '*'; - } - } - - // Get language ID from text - $wordlc = mb_strtolower($wordText, 'UTF-8'); - $bindings = [$textId]; - $langId = (int) Connection::preparedFetchValue( - "SELECT TxLgID FROM texts WHERE TxID = ?" - . UserScopedQuery::forTablePrepared('texts', $bindings), - $bindings, - 'TxLgID' - ); - - // Create the term using VocabularyFacade - $term = $this->vocabularyFacade->createTerm( - $langId, - $wordText, - $status, - $translation, - '', // sentence - '', // notes - '', // romanization - 1 // wordCount (single word) - ); - - $wid = $term->id()->toInt(); - - // Link to text items (cross-module operation) - Connection::preparedExecute( - "UPDATE word_occurrences SET Ti2WoID = ? - WHERE Ti2LgID = ? AND LOWER(Ti2Text) = ?", - [$wid, $langId, $wordlc] - ); - - $hex = StringUtils::toClassName( - Escaping::prepareTextdata($wordlc) - ); - - return [ - 'wid' => $wid, - 'word' => $wordText, - 'wordRaw' => $wordText, - 'translation' => $translation, - 'status' => $status, - 'hex' => $hex - ]; - } - - /** - * Check if this is a new word (status 1) that should set no-cache headers. - * - * @param int $status Word status - * - * @return bool True if no-cache headers should be set - */ - public function shouldSetNoCacheHeaders(int $status): bool - { - return $status === 1; - } -} diff --git a/src/Modules/Vocabulary/Http/MultiWordController.php b/src/Modules/Vocabulary/Http/MultiWordController.php deleted file mode 100644 index d1a08baa8..000000000 --- a/src/Modules/Vocabulary/Http/MultiWordController.php +++ /dev/null @@ -1,355 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - */ - -declare(strict_types=1); - -namespace Lwt\Modules\Vocabulary\Http; - -use Lwt\Shared\Infrastructure\Http\InputValidator; -use Lwt\Shared\Infrastructure\Database\Escaping; -use Lwt\Shared\Infrastructure\Database\Settings; -use Lwt\Modules\Vocabulary\Application\VocabularyFacade; -use Lwt\Modules\Vocabulary\Application\Services\ExportService; -use Lwt\Shared\Infrastructure\Dictionary\DictionaryAdapter; -use Lwt\Modules\Language\Application\LanguageFacade; -use Lwt\Modules\Tags\Application\TagsFacade; -use Lwt\Shared\UI\Helpers\PageLayoutHelper; - -/** - * Controller for multi-word expression management. - * - * Handles: - * - /word/edit-multi - Create/edit multi-word expressions - * - /word/delete-multi - Delete multi-word expressions - * - * @since 3.0.0 - */ -class MultiWordController extends VocabularyBaseController -{ - /** - * Vocabulary facade. - */ - private VocabularyFacade $facade; - - /** - * Adapters. - */ - private DictionaryAdapter $dictionaryAdapter; - - /** - * Services. - */ - private LanguageFacade $languageFacade; - - /** - * Constructor. - * - * @param VocabularyFacade|null $facade Vocabulary facade - * @param DictionaryAdapter|null $dictionaryAdapter Dictionary adapter - * @param LanguageFacade|null $languageFacade Language facade - */ - public function __construct( - ?VocabularyFacade $facade = null, - ?DictionaryAdapter $dictionaryAdapter = null, - ?LanguageFacade $languageFacade = null - ) { - parent::__construct(); - $this->facade = $facade ?? new VocabularyFacade(); - $this->dictionaryAdapter = $dictionaryAdapter ?? new DictionaryAdapter(); - $this->languageFacade = $languageFacade ?? new LanguageFacade(); - } - - /** - * Edit multi-word expression. - * - * @param array $params Route parameters - * - * @return void - */ - public function editMulti(array $params): void - { - $op = InputValidator::getString('op'); - if ($op !== '') { - // Handle save/update operation - $this->handleMultiWordOperation(); - } else { - // Display form - $this->displayMultiWordForm(); - } - - PageLayoutHelper::renderPageEnd(); - } - - /** - * Handle multi-word save/update operation. - * - * @return void - */ - private function handleMultiWordOperation(): void - { - $textlc = trim(InputValidator::getString('WoTextLC')); - $text = trim(InputValidator::getString('WoText')); - - // Validate lowercase matches - if (mb_strtolower($text, 'UTF-8') != $textlc) { - $titletext = "New/Edit Term: " . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - echo '
' . - '' . - 'Error: Term in lowercase must be exactly = "' . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8') . - '", please go back and correct this!
'; - return; - } - - $translationRaw = ExportService::replaceTabNewline(InputValidator::getString('WoTranslation')); - $translation = ($translationRaw == '') ? '*' : $translationRaw; - - $woText = InputValidator::getString('WoText'); - $woRomanization = InputValidator::getString('WoRomanization'); - $woSentence = InputValidator::getString('WoSentence'); - $woStatus = InputValidator::getInt('WoStatus', 0) ?? 0; - $data = [ - 'text' => Escaping::prepareTextdata($woText), - 'textlc' => Escaping::prepareTextdata($textlc), - 'translation' => $translation, - 'roman' => $woRomanization, - 'sentence' => $woSentence, - ]; - - $op = InputValidator::getString('op'); - $multiWordService = $this->getMultiWordService(); - $contextService = $this->getContextService(); - - if ($op == 'Save') { - // Insert new multi-word - $data['status'] = $woStatus; - $data['lgid'] = InputValidator::getInt('WoLgID', 0) ?? 0; - $data['wordcount'] = InputValidator::getInt('len', 0) ?? 0; - - $titletext = "New Term: " . htmlspecialchars($data['textlc'], ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - - $result = $multiWordService->createMultiWord($data); - $wid = $result['id']; - } else { - // Update existing multi-word - $wid = InputValidator::getInt('WoID', 0) ?? 0; - $oldStatus = InputValidator::getInt('WoOldStatus', 0) ?? 0; - $newStatus = $woStatus; - - $titletext = "Edit Term: " . htmlspecialchars($data['textlc'], ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - - $result = $multiWordService->updateMultiWord($wid, $data, $oldStatus, $newStatus); - - // Prepare data for view - $tagList = TagsFacade::getWordTagList($wid, false); - $formattedTags = $tagList !== '' ? ' [' . $tagList . ']' : ''; - $termJson = $contextService->exportTermAsJson( - $wid, - $data['text'], - $data['roman'], - $translation . $formattedTags, - $newStatus - ); - $oldStatusValue = $oldStatus; - - $this->render('edit_multi_update_result', [ - 'wid' => $wid, - 'result' => $result, - 'termJson' => $termJson, - 'oldStatusValue' => $oldStatusValue, - ]); - } - } - - /** - * Display multi-word edit form (new or existing). - * - * @return void - */ - private function displayMultiWordForm(): void - { - $tid = InputValidator::getInt('tid', 0) ?? 0; - $ord = InputValidator::getInt('ord', 0) ?? 0; - $strWid = InputValidator::getString('wid'); - $contextService = $this->getContextService(); - $multiWordService = $this->getMultiWordService(); - - // Determine if we're editing an existing word or creating new - if ($strWid == "" || !is_numeric($strWid)) { - // No ID provided: check if text exists in database - $lgid = $contextService->getLanguageIdFromText($tid); - $txtParam = InputValidator::getString('txt'); - $textlc = mb_strtolower( - Escaping::prepareTextdata($txtParam), - 'UTF-8' - ); - - $strWid = $multiWordService->findMultiWordByText($textlc, (int) $lgid); - } - - if ($strWid === null) { - // New multi-word - $txtParam = InputValidator::getString('txt'); - $len = InputValidator::getInt('len', 0) ?? 0; - PageLayoutHelper::renderPageStartNobody("New Term: " . $txtParam); - $this->displayNewMultiWordForm($txtParam, $tid, $ord, $len); - } else { - // Edit existing multi-word - $wid = (int) $strWid; - $wordData = $multiWordService->getMultiWordData($wid); - if ($wordData === null) { - throw new \RuntimeException("Cannot access term and language: multi-word not found"); - } - PageLayoutHelper::renderPageStartNobody("Edit Term: " . $wordData['text']); - $this->displayEditMultiWordForm($wid, $wordData, $tid, $ord); - } - } - - /** - * Display form for new multi-word. - * - * @param string $text Original text - * @param int $tid Text ID - * @param int $ord Text order - * @param int $len Number of words - * - * @return void - */ - private function displayNewMultiWordForm(string $text, int $tid, int $ord, int $len): void - { - $contextService = $this->getContextService(); - $multiWordService = $this->getMultiWordService(); - $lgid = $contextService->getLanguageIdFromText($tid); - $termText = Escaping::prepareTextdata($text); - $textlc = mb_strtolower($termText, 'UTF-8'); - - // Check if word already exists - $existingWid = $multiWordService->findMultiWordByText($textlc, (int) $lgid); - if ($existingWid !== null) { - // Get text from existing word - $wordData = $multiWordService->getMultiWordData($existingWid); - if ($wordData !== null) { - /** @var string $termText */ - $termText = $wordData['text']; - } - } - - $scrdir = $this->languageFacade->getScriptDirectionTag((int) $lgid); - $seid = $contextService->getSentenceIdAtPosition($tid, $ord) ?? 0; - $sent = $this->getSentenceService()->formatSentence( - $seid, - $textlc, - (int) Settings::getWithDefault('set-term-sentence-count') - ); - $showRoman = $contextService->shouldShowRomanization($tid); - - // Variables for view - $term = (object) [ - 'lgid' => $lgid, - 'text' => $termText, - 'textlc' => $textlc, - 'id' => $existingWid - ]; - $sentence = ExportService::replaceTabNewline($sent[1] ?? ''); - - $similarTermsRow = (new \Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms())->getTableRow(); - $dictLinksHtml = $this->dictionaryAdapter->createDictLinksInEditWin( - (int) $lgid, - $termText, - 'document.forms[0].WoSentence', - !InputValidator::hasFromGet('nodict') - ); - $sentenceAreaHtml = $this->getSentenceService()->renderExampleSentencesArea( - (int) $lgid, - $textlc, - 'document.forms.newword.WoSentence', - -1 - ); - $wordTagsHtml = TagsFacade::getWordTagsHtml(0); - - $this->render('form_edit_multi_new', [ - 'term' => $term, - 'sentence' => $sentence, - 'scrdir' => $scrdir, - 'showRoman' => $showRoman, - 'tid' => $tid, - 'ord' => $ord, - 'len' => $len, - 'similarTermsRow' => $similarTermsRow, - 'dictLinksHtml' => $dictLinksHtml, - 'sentenceAreaHtml' => $sentenceAreaHtml, - 'wordTagsHtml' => $wordTagsHtml, - ]); - } - - /** - * Display form for editing existing multi-word. - * - * @param int $wid Word ID - * @param array{ - * text: string, lgid: int, translation: string, sentence: string, - * notes: string, romanization: string, status: int - * } $wordData Word data from service - * @param int $tid Text ID - * @param int $ord Text order - * - * @return void - */ - private function displayEditMultiWordForm(int $wid, array $wordData, int $tid, int $ord): void - { - $lgid = $wordData['lgid']; - $termText = $wordData['text']; - $textlc = mb_strtolower($termText, 'UTF-8'); - - $scrdir = $this->languageFacade->getScriptDirectionTag($lgid); - $showRoman = $this->getContextService()->shouldShowRomanization($tid); - - $similarTermsRow = (new \Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms())->getTableRow(); - $dictLinksHtml = $this->dictionaryAdapter->createDictLinksInEditWin( - $lgid, - $termText, - 'document.forms[0].WoSentence', - !InputValidator::hasFromGet('nodict') - ); - $sentenceAreaHtml = $this->getSentenceService()->renderExampleSentencesArea( - $lgid, - $textlc, - 'document.forms.editword.WoSentence', - $wid - ); - $wordTagsHtml = TagsFacade::getWordTagsHtml($wid); - - $this->render('form_edit_multi_existing', [ - 'wid' => $wid, - 'wordData' => $wordData, - 'termText' => $termText, - 'textlc' => $textlc, - 'lgid' => $lgid, - 'scrdir' => $scrdir, - 'showRoman' => $showRoman, - 'tid' => $tid, - 'ord' => $ord, - 'similarTermsRow' => $similarTermsRow, - 'dictLinksHtml' => $dictLinksHtml, - 'sentenceAreaHtml' => $sentenceAreaHtml, - 'wordTagsHtml' => $wordTagsHtml, - ]); - } -} diff --git a/src/Modules/Vocabulary/Http/TermCrudApiHandler.php b/src/Modules/Vocabulary/Http/TermCrudApiHandler.php index 750e6db1f..9b97fe840 100644 --- a/src/Modules/Vocabulary/Http/TermCrudApiHandler.php +++ b/src/Modules/Vocabulary/Http/TermCrudApiHandler.php @@ -26,6 +26,7 @@ use Lwt\Modules\Vocabulary\Application\VocabularyFacade; use Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms; use Lwt\Modules\Vocabulary\Application\Services\TermStatusService; +use Lwt\Modules\Vocabulary\Application\Services\WordBulkService; use Lwt\Modules\Tags\Application\TagsFacade; use Lwt\Modules\Vocabulary\Application\Services\WordContextService; use Lwt\Modules\Vocabulary\Application\Services\WordDiscoveryService; @@ -50,6 +51,7 @@ class TermCrudApiHandler private WordContextService $contextService; private WordDiscoveryService $discoveryService; private WordLinkingService $linkingService; + private WordBulkService $bulkService; /** * Constructor. @@ -65,13 +67,15 @@ public function __construct( ?FindSimilarTerms $findSimilarTerms = null, ?WordContextService $contextService = null, ?WordDiscoveryService $discoveryService = null, - ?WordLinkingService $linkingService = null + ?WordLinkingService $linkingService = null, + ?WordBulkService $bulkService = null ) { $this->facade = $facade ?? new VocabularyFacade(); $this->findSimilarTerms = $findSimilarTerms ?? new FindSimilarTerms(); $this->contextService = $contextService ?? new WordContextService(); $this->discoveryService = $discoveryService ?? new WordDiscoveryService(); $this->linkingService = $linkingService ?? new WordLinkingService(); + $this->bulkService = $bulkService ?? new WordBulkService(); } // ========================================================================= @@ -430,7 +434,11 @@ public function formatQuickCreate(int $textId, int $position, int $status): arra /** * Get term data prepared for editing in modal. * - * @param int $textId Text ID + * An existing term carries its own language, so $textId may be 0 when + * $wordId is given. That is what lets callers with no reading context — + * the review screen, the term list — open the same editor. + * + * @param int $textId Text ID (may be 0 when $wordId is given) * @param int $position Position in text * @param int|null $wordId Word ID (for existing terms) * @@ -438,17 +446,32 @@ public function formatQuickCreate(int $textId, int $position, int $status): arra */ public function getTermForEdit(int $textId, int $position, ?int $wordId = null): array { - // Get language ID and settings from text - $textData = QueryBuilder::table('texts') - ->select(['TxLgID', 'TxTitle']) - ->where('TxID', '=', $textId) - ->firstPrepared(); + $hasWordId = $wordId !== null && $wordId > 0; - if ($textData === null) { - return ['error' => 'Text not found']; - } + if ($hasWordId) { + $wordLang = QueryBuilder::table('words') + ->select(['WoLgID']) + ->where('WoID', '=', $wordId) + ->firstPrepared(); + + if ($wordLang === null) { + return ['error' => 'Term not found']; + } - $langId = (int) $textData['TxLgID']; + $langId = (int) $wordLang['WoLgID']; + } else { + // A new term is only identifiable through its position in a text. + $textData = QueryBuilder::table('texts') + ->select(['TxLgID', 'TxTitle']) + ->where('TxID', '=', $textId) + ->firstPrepared(); + + if ($textData === null) { + return ['error' => 'Text not found']; + } + + $langId = (int) $textData['TxLgID']; + } // Get language settings $langData = QueryBuilder::table('languages') @@ -472,7 +495,7 @@ public function getTermForEdit(int $textId, int $position, ?int $wordId = null): ]; // If word ID provided, get existing term data - if ($wordId !== null && $wordId > 0) { + if ($hasWordId) { $termData = QueryBuilder::table('words') ->select([ 'WoID', 'WoText', 'WoTextLC', 'WoLemma', 'WoLemmaLC', 'WoTranslation', @@ -771,6 +794,21 @@ public function updateTermFull(int $termId, array $data): array return ['error' => 'Status must be 1-5, 98, or 99']; } + $textLc = (string) $existing['WoTextLC']; + + // The term text may only be recased, never rewritten: WoTextLC is what + // textitems2 is linked on, so changing it would orphan the occurrences. + $text = (string) $existing['WoText']; + if (isset($data['text']) && is_scalar($data['text'])) { + $candidate = trim((string) $data['text']); + if ($candidate !== '') { + if (mb_strtolower($candidate, 'UTF-8') !== $textLc) { + return ['error' => 'Term in lowercase must be exactly "' . $textLc . '"']; + } + $text = $candidate; + } + } + $translation = trim((string)($data['translation'] ?? '')); if ($translation === '') { $translation = '*'; @@ -786,9 +824,10 @@ public function updateTermFull(int $termId, array $data): array $scoreUpdate = TermStatusService::makeScoreRandomInsertUpdate('u'); // Use raw SQL for dynamic score update - $bindings = [$translation, $romanization, $sentence, $notes, $lemma, $lemmaLc, $status, $termId]; + $bindings = [$text, $translation, $romanization, $sentence, $notes, $lemma, $lemmaLc, $status, $termId]; Connection::preparedExecute( "UPDATE words SET + WoText = ?, WoTranslation = ?, WoRomanization = ?, WoSentence = ?, @@ -800,7 +839,7 @@ public function updateTermFull(int $termId, array $data): array {$scoreUpdate} WHERE WoID = ?" . UserScopedQuery::forTablePrepared('words', $bindings), - [$translation, $romanization, $sentence, $notes, $lemma, $lemmaLc, $status, $termId] + [$text, $translation, $romanization, $sentence, $notes, $lemma, $lemmaLc, $status, $termId] ); // Save tags if provided @@ -819,11 +858,11 @@ public function updateTermFull(int $termId, array $data): array 'success' => true, 'term' => [ 'id' => $termId, - 'text' => (string) $existing['WoText'], - 'textLc' => (string) $existing['WoTextLC'], + 'text' => $text, + 'textLc' => $textLc, 'lemma' => $lemma ?? '', 'lemmaLc' => $lemmaLc ?? '', - 'hex' => StringUtils::toClassName((string) $existing['WoTextLC']), + 'hex' => StringUtils::toClassName($textLc), 'translation' => $translation === '*' ? '' : $translation, 'romanization' => $romanization, 'sentence' => $sentence, @@ -834,6 +873,61 @@ public function updateTermFull(int $termId, array $data): array ]; } + /** + * Create several terms at once, as the bulk-translate page does. + * + * @param array $data Request body with a `terms` list of + * {lg, text, status, trans} entries + * + * @return array Response data + */ + public function createTermsBulk(array $data): array + { + $rawTerms = $data['terms'] ?? null; + if (!is_array($rawTerms) || $rawTerms === []) { + return ['error' => 'No terms supplied']; + } + + $terms = []; + /** @var mixed $row */ + foreach ($rawTerms as $row) { + if (!is_array($row)) { + continue; + } + $text = trim((string) ($row['text'] ?? '')); + $langId = (int) ($row['lg'] ?? 0); + if ($text === '' || $langId <= 0) { + continue; + } + $status = (int) ($row['status'] ?? 1); + if (!TermStatusService::isValidStatus($status)) { + return ['error' => 'Status must be 1-5, 98, or 99']; + } + $terms[] = [ + 'lg' => $langId, + 'text' => $text, + 'status' => $status, + 'trans' => trim((string) ($row['trans'] ?? '')), + ]; + } + + if ($terms === []) { + return ['error' => 'No terms supplied']; + } + + $maxWoId = $this->bulkService->bulkSaveTerms($terms); + $newWords = $this->bulkService->getNewWordsAfter($maxWoId); + + // Newly created terms have to be attached to their occurrences, or the + // reading view keeps showing them as unknown. + $this->linkingService->linkNewWordsToTextItems($maxWoId); + + return [ + 'success' => true, + 'saved' => count($newWords), + ]; + } + /** * Format response for getting term data for editing. * diff --git a/src/Modules/Vocabulary/Http/TermDisplayController.php b/src/Modules/Vocabulary/Http/TermDisplayController.php index 84a5a56cf..7e59f7989 100644 --- a/src/Modules/Vocabulary/Http/TermDisplayController.php +++ b/src/Modules/Vocabulary/Http/TermDisplayController.php @@ -21,7 +21,6 @@ use Lwt\Shared\Infrastructure\Database\Settings; use Lwt\Shared\Infrastructure\Database\Validation; use Lwt\Modules\Vocabulary\Application\VocabularyFacade; -use Lwt\Modules\Vocabulary\Application\UseCases\CreateTermFromHover; use Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms; use Lwt\Shared\Infrastructure\Dictionary\DictionaryAdapter; use Lwt\Modules\Language\Application\LanguageFacade; @@ -51,7 +50,6 @@ class TermDisplayController extends VocabularyBaseController /** * Use cases. */ - private CreateTermFromHover $createTermFromHover; private FindSimilarTerms $findSimilarTerms; /** @@ -68,21 +66,18 @@ class TermDisplayController extends VocabularyBaseController * Constructor. * * @param VocabularyFacade|null $facade Vocabulary facade - * @param CreateTermFromHover|null $createTermFromHover Create term from hover use case * @param FindSimilarTerms|null $findSimilarTerms Find similar terms use case * @param DictionaryAdapter|null $dictionaryAdapter Dictionary adapter * @param LanguageFacade|null $languageFacade Language facade */ public function __construct( ?VocabularyFacade $facade = null, - ?CreateTermFromHover $createTermFromHover = null, ?FindSimilarTerms $findSimilarTerms = null, ?DictionaryAdapter $dictionaryAdapter = null, ?LanguageFacade $languageFacade = null ) { parent::__construct(); $this->facade = $facade ?? new VocabularyFacade(); - $this->createTermFromHover = $createTermFromHover ?? new CreateTermFromHover(); $this->findSimilarTerms = $findSimilarTerms ?? new FindSimilarTerms(); $this->dictionaryAdapter = $dictionaryAdapter ?? new DictionaryAdapter(); $this->languageFacade = $languageFacade ?? new LanguageFacade(); @@ -227,90 +222,6 @@ public function edit(array $params): void PageLayoutHelper::renderPageEnd(); } - /** - * Handle the hover create action from reading view. - * - * This is the route handler that parses request params and - * renders the result view. - * - * @param array $params Route parameters - * - * @return void - */ - public function hoverCreate(array $params): void - { - $text = InputValidator::getString('text'); - $textId = InputValidator::getInt('tid', 0) ?? 0; - $status = InputValidator::getInt('status', 1) ?? 1; - $targetLang = InputValidator::getString('tl'); - $sourceLang = InputValidator::getString('sl'); - - // Create the term - $result = $this->createFromHover( - $textId, - $text, - $status, - $sourceLang, - $targetLang - ); - - // Render page - PageLayoutHelper::renderPageStart("New Term: " . (string)$result['word'], false); - - // Prepare view variables - $word = (string)$result['word']; - $wordRaw = (string)$result['wordRaw']; - $wid = (int)$result['wid']; - $hex = (string)$result['hex']; - $translation = (string)$result['translation']; - - $this->render('hover_save_result', [ - 'word' => $word, - 'wordRaw' => $wordRaw, - 'wid' => $wid, - 'hex' => $hex, - 'translation' => $translation, - 'textId' => $textId, - 'status' => $status, - 'todoContent' => $this->getTextStatisticsService()->getTodoWordsContent($textId), - ]); - - PageLayoutHelper::renderPageEnd(); - } - - /** - * Create a term from hover action in reading view. - * - * @param int $textId Text ID - * @param string $wordText Word text - * @param int $status Word status (1-5) - * @param string $sourceLang Source language code - * @param string $targetLang Target language code - * - * @return array Term creation result - */ - private function createFromHover( - int $textId, - string $wordText, - int $status, - string $sourceLang = '', - string $targetLang = '' - ): array { - // Set no-cache headers for new words - if ($this->createTermFromHover->shouldSetNoCacheHeaders($status)) { - header('Pragma: no-cache'); - header('Expires: 0'); - } - - return $this->createTermFromHover->execute( - $textId, - $wordText, - $status, - $sourceLang, - $targetLang - ); - } - /** * Get similar terms for a given term. * diff --git a/src/Modules/Vocabulary/Http/TermEditController.php b/src/Modules/Vocabulary/Http/TermEditController.php index fe7192095..9a89f6921 100644 --- a/src/Modules/Vocabulary/Http/TermEditController.php +++ b/src/Modules/Vocabulary/Http/TermEditController.php @@ -19,16 +19,10 @@ use Lwt\Shared\Infrastructure\Http\InputValidator; use Lwt\Shared\Infrastructure\Http\RedirectResponse; -use Lwt\Shared\Infrastructure\Database\Connection; -use Lwt\Shared\Infrastructure\Database\QueryBuilder; -use Lwt\Shared\Infrastructure\Database\Escaping; use Lwt\Shared\Infrastructure\Database\Settings; use Lwt\Modules\Vocabulary\Application\VocabularyFacade; -use Lwt\Modules\Vocabulary\Application\Services\TermStatusService; -use Lwt\Modules\Vocabulary\Application\Services\ExportService; use Lwt\Shared\Infrastructure\Dictionary\DictionaryAdapter; use Lwt\Modules\Language\Application\LanguageFacade; -use Lwt\Shared\Infrastructure\Language\LanguagePresets; use Lwt\Modules\Tags\Application\TagsFacade; use Lwt\Shared\UI\Helpers\PageLayoutHelper; @@ -80,322 +74,74 @@ public function __construct( } /** - * Edit word by ID. + * Render the standalone term editor. * - * Route: GET/POST /words/{id}/edit + * The page carries only the identifiers; termEditPage loads the term from + * GET /api/v1/terms/for-edit and mounts the same editor the reading view + * opens in a modal, so there is one editor rather than three forms. * - * @param int $id Word ID from route parameter + * @param int $textId Text ID, or 0 when editing an existing term + * @param int $position Word position in the text, or 0 + * @param int|null $wordId Term ID, or null when creating from a position + * @param string $returnUrl Where to go once editing finishes * * @return void */ - public function editWordById(int $id): void + private function renderEditorPage(int $textId, int $position, ?int $wordId, string $returnUrl): void { - $op = InputValidator::getString('op'); + PageLayoutHelper::renderPageStart(__('vocabulary.form.edit_term'), true, 'words'); - if ($op !== '') { - if ($this->handleEditWordOperation()) { - return; // Error was rendered with full page - } - } else { - $this->displayEditWordForm($id, 0, 0, ''); - } + $this->render('edit_page', [ + 'textId' => $textId, + 'position' => $position, + 'wordId' => $wordId, + 'returnUrl' => $returnUrl, + ]); PageLayoutHelper::renderPageEnd(); } /** - * Edit word form. + * Edit word by ID. * - * Handles: - * - Display edit form: ?wid=[wordid] or ?tid=[textid]&ord=[ord] - * - Save/Update: ?op=Save or ?op=Change + * Route: GET /words/{id}/edit * - * @param array $params Route parameters + * @param int $id Word ID from route parameter * * @return void */ - public function editWord(array $params): void + public function editWordById(int $id): void { - $wid = InputValidator::getString('wid'); - $tid = InputValidator::getString('tid'); - $ord = InputValidator::getString('ord'); - $op = InputValidator::getString('op'); - - // Check for valid entry point - if ($wid === '' && $tid . $ord === '' && $op === '') { - return; - } - - $fromAnn = InputValidator::getString('fromAnn'); - - if ($op !== '') { - if ($this->handleEditWordOperation()) { - return; // Error was rendered with full page - } - } else { - $widInt = ($wid !== '' && is_numeric($wid)) ? (int) $wid : -1; - $textId = InputValidator::getInt('tid', 0) ?? 0; - $ordInt = InputValidator::getInt('ord', 0) ?? 0; - $this->displayEditWordForm($widInt, $textId, $ordInt, $fromAnn); - } - - PageLayoutHelper::renderPageEnd(); + $this->renderEditorPage(0, 0, $id, '/words'); } /** - * Handle save/update operation for word edit. + * Edit word form: ?wid=[wordid] or ?tid=[textid]&ord=[ord]. * - * @return bool True if error response was rendered, false otherwise - */ - private function handleEditWordOperation(): bool - { - $textlc = trim(Escaping::prepareTextdata(InputValidator::getString('WoTextLC'))); - $text = trim(Escaping::prepareTextdata(InputValidator::getString('WoText'))); - - // Validate lowercase matches - if (mb_strtolower($text, 'UTF-8') != $textlc) { - $titletext = "New/Edit Term: " . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - echo '
' . - '' . - 'Error: Term in lowercase must be exactly = "' . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8') . - '", please go back and correct this!
'; - PageLayoutHelper::renderPageEnd(); - return true; - } - - $translation = ExportService::replaceTabNewline(InputValidator::getString('WoTranslation')); - if ($translation == '') { - $translation = '*'; - } - - $op = InputValidator::getString('op'); - $requestData = $this->getWordFormData(); - - if ($op == 'Save') { - // Insert new term - $result = $this->getCrudService()->create($requestData); - $hex = $this->getContextService()->textToClassName(InputValidator::getString('WoTextLC')); - $oldStatus = 0; - $titletext = "New Term: " . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8'); - } else { - // Update existing term - $result = $this->getCrudService()->update(InputValidator::getInt('WoID', 0) ?? 0, $requestData); - $hex = null; - $oldStatus = InputValidator::getString('WoOldStatus'); - $titletext = "Edit Term: " . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8'); - } - - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - - $wid = $result['id']; - $message = $result['message']; - - TagsFacade::saveWordTagsFromForm($wid); - - // Prepare view variables - $textId = InputValidator::getInt('tid', 0) ?? 0; - $status = InputValidator::getString('WoStatus'); - $romanization = InputValidator::getString('WoRomanization'); - $fromAnn = InputValidator::getString('fromAnn'); - - $tagList = TagsFacade::getWordTagList($wid, false); - $todoContent = $this->getTextStatisticsService()->getTodoWordsContent($textId); - - $this->render('edit_result', [ - 'wid' => $wid, - 'message' => $message, - 'textId' => $textId, - 'status' => $status, - 'romanization' => $romanization, - 'translation' => $translation, - 'hex' => $hex, - 'oldStatus' => $oldStatus, - 'isNew' => ($op == 'Save'), - 'fromAnn' => $fromAnn, - 'text' => $text, - 'textlc' => $textlc, - 'tagList' => $tagList, - 'todoContent' => $todoContent, - ]); - - return false; - } - - /** - * Display the word edit form (new or existing). - * - * @param int $wid Word ID (-1 for new) - * @param int $textId Text ID - * @param int $ord Word order position - * @param string $fromAnn From annotation flag + * @param array $params Route parameters * * @return void */ - private function displayEditWordForm(int $wid, int $textId, int $ord, string $fromAnn): void + public function editWord(array $params): void { - $crudService = $this->getCrudService(); - $contextService = $this->getContextService(); - $linkingService = $this->getLinkingService(); - - if ($wid == -1) { - // Get the term from text items - $termData = $linkingService->getTermFromTextItem($textId, $ord); - if ($termData === null) { - throw new \RuntimeException("Cannot access term and language: term not found in text"); - } - $term = (string) $termData['Ti2Text']; - $lang = (int) $termData['Ti2LgID']; - $termlc = mb_strtolower($term, 'UTF-8'); - - // Check if word already exists - $existingId = $crudService->findByText($termlc, $lang); - if ($existingId !== null) { - $new = false; - $wid = $existingId; - } else { - $new = true; - } - } else { - // Get existing word data - $wordData = $crudService->findById($wid); - if ($wordData === null) { - throw new \RuntimeException("Cannot access term and language: word ID not found"); - } - $term = (string) $wordData['WoText']; - $lang = (int) $wordData['WoLgID']; - $termlc = mb_strtolower($term, 'UTF-8'); - $new = false; - } - - $titletext = ($new ? "New Term" : "Edit Term") . ": " . htmlspecialchars($term, ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - - $scrdir = $this->languageFacade->getScriptDirectionTag($lang); - $langData = $contextService->getLanguageData($lang); - $showRoman = $langData['showRoman']; - - if ($new) { - // New word form - $sentence = $contextService->getSentenceForTerm($textId, $ord, $termlc); - $transUri = $langData['translateUri']; - $lgname = $langData['name']; - $langShort = array_key_exists($lgname, LanguagePresets::getAll()) ? - LanguagePresets::getAll()[$lgname][1] : ''; - - $similarTermsRow = (new \Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms())->getTableRow(); - $dictLinksHtml = $this->dictionaryAdapter->createDictLinksInEditWin( - $lang, - $term, - 'document.forms[0].WoSentence', - !InputValidator::hasFromGet('nodict') - ); - $sentenceAreaHtml = $this->getSentenceService()->renderExampleSentencesArea( - $lang, - $termlc, - 'document.forms.newword.WoSentence', - 0 - ); - $wordTagsHtml = TagsFacade::getWordTagsHtml(0); - - $this->render('form_edit_new', [ - 'term' => $term, - 'termlc' => $termlc, - 'lang' => $lang, - 'sentence' => $sentence, - 'transUri' => $transUri, - 'lgname' => $lgname, - 'langShort' => $langShort, - 'scrdir' => $scrdir, - 'showRoman' => $showRoman, - 'textId' => $textId, - 'ord' => $ord, - 'fromAnn' => $fromAnn, - 'similarTermsRow' => $similarTermsRow, - 'dictLinksHtml' => $dictLinksHtml, - 'sentenceAreaHtml' => $sentenceAreaHtml, - 'wordTagsHtml' => $wordTagsHtml, - ]); - } else { - // Edit existing word form - $wordData = $crudService->findById($wid); - if ($wordData === null) { - throw new \RuntimeException("Cannot access word data: word ID not found"); - } - - $status = (int)$wordData['WoStatus']; - if ($fromAnn == '' && $status >= 98) { - $status = 1; - } - - $sentence = ExportService::replaceTabNewline((string)$wordData['WoSentence']); - if ($sentence == '' && $textId !== 0 && $ord !== 0) { - $sentence = $contextService->getSentenceForTerm($textId, $ord, $termlc); - } - - $transl = ExportService::replaceTabNewline((string)$wordData['WoTranslation']); - if ($transl == '*') { - $transl = ''; - } + $wid = InputValidator::getInt('wid', 0) ?? 0; + $textId = InputValidator::getInt('tid', 0) ?? 0; + $ord = InputValidator::getInt('ord', 0) ?? 0; - // Get showRoman from language joined with text - $showRoman = (bool) QueryBuilder::table('languages') - ->join('texts', 'TxLgID', '=', 'LgID') - ->where('TxID', '=', $textId) - ->valuePrepared('LgShowRomanization'); - - $similarTermsRow = (new \Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms())->getTableRow(); - if ($fromAnn !== '') { - $dictLinksHtml = $this->dictionaryAdapter->createDictLinksInEditWin2( - $lang, - 'WoSentence', - 'WoText' - ); - } else { - $dictLinksHtml = $this->dictionaryAdapter->createDictLinksInEditWin( - $lang, - $term, - 'WoSentence', - !InputValidator::hasFromGet('nodict') - ); - } - $sentenceAreaHtml = $this->getSentenceService()->renderExampleSentencesArea( - $lang, - $termlc, - 'WoSentence', - $wid + // Nothing identifies a term: neither an ID nor a position in a text. + // Say so rather than serving a blank page. + if ($wid <= 0 && ($textId <= 0 || $ord <= 0)) { + throw new \RuntimeException( + 'Cannot edit term: expected a term ID, or a text ID with a position' ); - $wordTagsHtml = TagsFacade::getWordTagsHtml($wid); - - $this->render('form_edit_existing', [ - 'wid' => $wid, - 'term' => $term, - 'termlc' => $termlc, - 'lang' => $lang, - 'status' => $status, - 'sentence' => $sentence, - 'transl' => $transl, - 'wordData' => $wordData, - 'scrdir' => $scrdir, - 'showRoman' => $showRoman, - 'textId' => $textId, - 'ord' => $ord, - 'fromAnn' => $fromAnn, - 'similarTermsRow' => $similarTermsRow, - 'dictLinksHtml' => $dictLinksHtml, - 'sentenceAreaHtml' => $sentenceAreaHtml, - 'wordTagsHtml' => $wordTagsHtml, - ]); } + + $returnUrl = $textId > 0 ? '/text/' . $textId . '/read' : '/words'; + $this->renderEditorPage($textId, $ord, $wid > 0 ? $wid : null, $returnUrl); } /** - * Edit term while testing. - * - * Call: ?wid=[wordid] - display edit form - * ?op=Change - update the term + * Edit term while reviewing: ?wid=[wordid]. * * @param array $params Route parameters * @@ -403,211 +149,13 @@ private function displayEditWordForm(int $wid, int $textId, int $ord, string $fr */ public function editTerm(array $params): void { - $translation_raw = ExportService::replaceTabNewline(InputValidator::getString('WoTranslation')); - $translation = ($translation_raw == '') ? '*' : $translation_raw; - - $op = InputValidator::getString('op'); - if ($op !== '') { - if ($this->handleEditTermOperation($translation)) { - return; // Error was rendered with full page - } - } else { - $this->displayEditTermForm(); - } - - PageLayoutHelper::renderPageEnd(); - } - - /** - * Handle update operation for edit term. - * - * @param string $translation Translation value - * - * @return bool True if error response was rendered, false otherwise - */ - private function handleEditTermOperation(string $translation): bool - { - $woTextLC = InputValidator::getString('WoTextLC'); - $woText = InputValidator::getString('WoText'); - $textlc = trim(Escaping::prepareTextdata($woTextLC)); - $text = trim(Escaping::prepareTextdata($woText)); - - if (mb_strtolower($text, 'UTF-8') != $textlc) { - $escapedText = htmlspecialchars(Escaping::prepareTextdata($woTextLC), ENT_QUOTES, 'UTF-8'); - $titletext = "New/Edit Term: " . $escapedText; - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - echo '
' . - '' . - 'Error: Term in lowercase must be exactly = "' . htmlspecialchars($textlc, ENT_QUOTES, 'UTF-8') . - '", please go back and correct this!
'; - PageLayoutHelper::renderPageEnd(); - return true; - } - - $op = InputValidator::getString('op'); - if ($op == 'Change') { - $titletext = "Edit Term: " . htmlspecialchars(Escaping::prepareTextdata($woTextLC), ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - echo '

' . $titletext . '

'; - - $oldstatus = InputValidator::getString('WoOldStatus'); - $newstatus = InputValidator::getString('WoStatus'); - $woId = InputValidator::getInt('WoID', 0) ?? 0; - $woSentence = InputValidator::getString('WoSentence'); - $woRomanization = InputValidator::getString('WoRomanization'); - - $scoreRandomUpdate = TermStatusService::makeScoreRandomInsertUpdate('u'); - $sentenceEscaped = ExportService::replaceTabNewline($woSentence); - - if ($oldstatus != $newstatus) { - // Status changed - update with status change timestamp - $bindings = [ - $woText, $translation, $sentenceEscaped, $woRomanization, - $newstatus, $woId - ]; - $sql = "UPDATE words SET - WoText = ?, WoTranslation = ?, WoSentence = ?, WoRomanization = ?, - WoStatus = ?, WoStatusChanged = NOW(), {$scoreRandomUpdate} - WHERE WoID = ?" - . \Lwt\Shared\Infrastructure\Database\UserScopedQuery::forTablePrepared('words', $bindings); - Connection::preparedExecute($sql, $bindings); - } else { - // Status unchanged - $bindings = [ - $woText, $translation, $sentenceEscaped, $woRomanization, - $woId - ]; - $sql = "UPDATE words SET - WoText = ?, WoTranslation = ?, WoSentence = ?, WoRomanization = ?, - {$scoreRandomUpdate} - WHERE WoID = ?" - . \Lwt\Shared\Infrastructure\Database\UserScopedQuery::forTablePrepared('words', $bindings); - Connection::preparedExecute($sql, $bindings); - } - $wid = $woId; - TagsFacade::saveWordTagsFromForm($wid); + $wid = InputValidator::getInt('wid', 0) ?? 0; - $message = 'Updated'; - - /** @var int|null $lang */ - $lang = QueryBuilder::table('words') - ->where('WoID', '=', $wid) - ->valuePrepared('WoLgID'); - if (!isset($lang)) { - throw new \RuntimeException('Cannot retrieve language: word not found'); - } - /** @var string|null $regexword */ - $regexword = QueryBuilder::table('languages') - ->where('LgID', '=', $lang) - ->valuePrepared('LgRegexpWordCharacters'); - if (!isset($regexword)) { - throw new \RuntimeException('Cannot retrieve language data: language not found'); - } - $sent = htmlspecialchars(ExportService::replaceTabNewline($woSentence), ENT_QUOTES, 'UTF-8'); - $sent1 = str_replace( - "{", - ' [', - str_replace( - "}", - '] ', - ExportService::maskTermInSentence($sent, $regexword) - ) - ); - - $status = $newstatus; - $romanization = $woRomanization; - $text = $woText; - $tagList = TagsFacade::getWordTagList($wid, false); - - $this->render('edit_term_result', [ - 'wid' => $wid, - 'message' => $message, - 'status' => $status, - 'romanization' => $romanization, - 'translation' => $translation, - 'text' => $text, - 'sent1' => $sent1, - 'tagList' => $tagList, - ]); + if ($wid <= 0) { + throw new \RuntimeException('Cannot edit term: expected a term ID'); } - return false; - } - - /** - * Display the edit term form. - * - * @return void - */ - private function displayEditTermForm(): void - { - $widParam = InputValidator::getString('wid'); - - if ($widParam == '') { - throw new \RuntimeException("Term ID missing: required parameter not provided"); - } - $wid = (int) $widParam; - - $record = QueryBuilder::table('words') - ->select(['WoText', 'WoLgID', 'WoTranslation', 'WoSentence', 'WoNotes', 'WoRomanization', 'WoStatus']) - ->where('WoID', '=', $wid) - ->firstPrepared(); - if ($record !== null) { - $term = (string) $record['WoText']; - $lang = (int) $record['WoLgID']; - $transl = ExportService::replaceTabNewline((string)$record['WoTranslation']); - if ($transl == '*') { - $transl = ''; - } - $sentence = ExportService::replaceTabNewline((string)$record['WoSentence']); - $notes = ExportService::replaceTabNewline((string)($record['WoNotes'] ?? '')); - $rom = (string)$record['WoRomanization']; - $status = (int)$record['WoStatus']; - $showRoman = (bool) QueryBuilder::table('languages') - ->where('LgID', '=', $lang) - ->valuePrepared('LgShowRomanization'); - } else { - throw new \RuntimeException("Term data not found: invalid term ID"); - } - - $termlc = mb_strtolower($term, 'UTF-8'); - $titletext = "Edit Term: " . htmlspecialchars($term, ENT_QUOTES, 'UTF-8'); - PageLayoutHelper::renderPageStartNobody($titletext); - $scrdir = $this->languageFacade->getScriptDirectionTag($lang); - - $similarTermsRow = (new \Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms())->getTableRow(); - $dictLinksHtml = $this->dictionaryAdapter->createDictLinksInEditWin( - $lang, - $term, - 'document.forms[0].WoSentence', - true - ); - $sentenceAreaHtml = $this->getSentenceService()->renderExampleSentencesArea( - $lang, - $termlc, - 'document.forms.editword.WoSentence', - $wid - ); - $wordTagsHtml = TagsFacade::getWordTagsHtml($wid); - - $this->render('form_edit_term', [ - 'wid' => $wid, - 'term' => $term, - 'termlc' => $termlc, - 'lang' => $lang, - 'transl' => $transl, - 'sentence' => $sentence, - 'notes' => $notes, - 'rom' => $rom, - 'status' => $status, - 'showRoman' => $showRoman, - 'scrdir' => $scrdir, - 'similarTermsRow' => $similarTermsRow, - 'dictLinksHtml' => $dictLinksHtml, - 'sentenceAreaHtml' => $sentenceAreaHtml, - 'wordTagsHtml' => $wordTagsHtml, - ]); + $this->renderEditorPage(0, 0, $wid, '/review'); } /** @@ -704,36 +252,6 @@ public function createWord(array $params): void $this->getExpressionService()->insertExpressions($result['textlc'], $woLgId, $wid, $len, 0); } elseif ($len == 1) { $this->getLinkingService()->linkToTextItems($wid, $woLgId, $result['textlc']); - - // Prepare view variables - $hex = $contextService->textToClassName($result['textlc']); - $translation = ExportService::replaceTabNewline(InputValidator::getString('WoTranslation')); - if ($translation === '') { - $translation = '*'; - } - $status = InputValidator::getString('WoStatus'); - $romanization = InputValidator::getString('WoRomanization'); - $text = $result['text']; - $textId = InputValidator::getInt('tid', 0) ?? 0; - $success = true; - $message = $result['message']; - $tagList = TagsFacade::getWordTagList($wid, false); - $todoContent = $this->getTextStatisticsService()->getTodoWordsContent($textId); - - $this->render('save_result', [ - 'wid' => $wid, - 'hex' => $hex, - 'translation' => $translation, - 'status' => $status, - 'romanization' => $romanization, - 'text' => $text, - 'textId' => $textId, - 'success' => $success, - 'message' => $message, - 'len' => $len, - 'tagList' => $tagList, - 'todoContent' => $todoContent, - ]); } } } else { diff --git a/src/Modules/Vocabulary/Http/TermImportController.php b/src/Modules/Vocabulary/Http/TermImportController.php index 983b413c4..ed72fee72 100644 --- a/src/Modules/Vocabulary/Http/TermImportController.php +++ b/src/Modules/Vocabulary/Http/TermImportController.php @@ -17,9 +17,7 @@ namespace Lwt\Modules\Vocabulary\Http; -use Lwt\Shared\Infrastructure\Utilities\StringUtils; use Lwt\Shared\Infrastructure\Http\InputValidator; -use Lwt\Shared\Infrastructure\Database\Escaping; use Lwt\Shared\Infrastructure\Database\Settings; use Lwt\Modules\Vocabulary\Application\Services\WordUploadService; use Lwt\Modules\Vocabulary\Application\Services\FrequencyLanguageMap; @@ -82,24 +80,10 @@ public function bulkTranslate(array $params): void $tid = InputValidator::getInt('tid', 0) ?? 0; $pos = InputValidator::getInt('offset'); - // Handle form submission (save terms) - $termsArray = InputValidator::getArray('term'); - if (!empty($termsArray)) { - /** @var array $terms */ - $terms = $termsArray; - $cnt = count($terms); + // Saving goes to POST /api/v1/terms/bulk and the next batch is a plain + // GET, so this only ever renders a batch of terms. + PageLayoutHelper::renderPageStartNobody('Translate New Words'); - if ($pos !== null) { - $pos -= $cnt; - } - - PageLayoutHelper::renderPageStart($cnt . ' New Word' . ($cnt == 1 ? '' : 's') . ' Saved', false); - $this->handleBulkSave($terms, $tid, $pos === null); - } else { - PageLayoutHelper::renderPageStartNobody('Translate New Words'); - } - - // Show next page of terms if there are more if ($pos !== null) { $sl = InputValidator::getString('sl'); $tl = InputValidator::getString('tl'); @@ -109,46 +93,6 @@ public function bulkTranslate(array $params): void PageLayoutHelper::renderPageEnd(); } - /** - * Handle saving bulk translated terms. - * - * @param array $terms Array of term data - * @param int $tid Text ID - * @param bool $cleanUp Whether to clean up right frames after save - * - * @return void - * - * @psalm-suppress UnusedParam $tid and $cleanUp are used in included view file - * @psalm-suppress UnresolvableInclude Path computed from viewPath property - */ - private function handleBulkSave(array $terms, int $tid, bool $cleanUp): void - { - $bulkService = $this->getBulkService(); - $maxWoId = $bulkService->bulkSaveTerms($terms); - - $tooltipMode = Settings::getWithDefault('set-tooltip-mode'); - $res = $bulkService->getNewWordsAfter($maxWoId); - - // Link new words to text items - $linkingService = new \Lwt\Modules\Vocabulary\Application\Services\WordLinkingService(); - $linkingService->linkNewWordsToTextItems($maxWoId); - - // Prepare data for view - /** @var list> $newWords */ - $newWords = []; - foreach ($res as $record) { - $record['hex'] = StringUtils::toClassName( - Escaping::prepareTextdata((string)$record['WoTextLC']) - ); - $record['translation'] = (string)$record['WoTranslation']; - $newWords[] = $record; - } - - $todoContent = $this->getTextStatisticsService()->getTodoWordsContent($tid); - - include $this->viewPath . 'bulk_save_result.php'; - } - /** * Display the bulk translate form. * diff --git a/src/Modules/Vocabulary/Http/TermStatusController.php b/src/Modules/Vocabulary/Http/TermStatusController.php index fb4d7e4da..cf0533d38 100644 --- a/src/Modules/Vocabulary/Http/TermStatusController.php +++ b/src/Modules/Vocabulary/Http/TermStatusController.php @@ -146,38 +146,4 @@ public function setReviewStatusView(array $params): void echo '

No status operation specified

'; PageLayoutHelper::renderPageEnd(); } - - /** - * Mark all words with status (well-known or ignore). - * - * @param array $params Route parameters - * - * @psalm-suppress UnresolvableInclude Path computed from viewPath property - * - * @return void - */ - public function markAllWords(array $params): void - { - $textId = InputValidator::getInt('text'); - if ($textId === null) { - return; - } - - $status = InputValidator::getInt('stat', 99) ?? 99; - - if ($status == 98) { - PageLayoutHelper::renderPageStart("Setting all blue words to Ignore", false); - } else { - PageLayoutHelper::renderPageStart("Setting all blue words to Well-known", false); - } - - $discoveryService = $this->getDiscoveryService(); - list($count, $wordsData) = $discoveryService->markAllWordsWithStatus($textId, $status); - $useTooltips = Settings::getWithDefault('set-tooltip-mode') == 1; - $todoContent = $this->getTextStatisticsService()->getTodoWordsContent($textId); - - include $this->viewPath . 'all_wellknown_result.php'; - - PageLayoutHelper::renderPageEnd(); - } } diff --git a/src/Modules/Vocabulary/Http/VocabularyApiRouter.php b/src/Modules/Vocabulary/Http/VocabularyApiRouter.php index 984eb30ad..1a769bb87 100644 --- a/src/Modules/Vocabulary/Http/VocabularyApiRouter.php +++ b/src/Modules/Vocabulary/Http/VocabularyApiRouter.php @@ -162,11 +162,13 @@ public function routePost(array $fragments, array $params): JsonResponse )); } elseif ($frag1 === 'full') { return Response::success($this->termHandler->formatCreateTermFull($params)); + } elseif ($frag1 === 'bulk') { + return Response::success($this->termHandler->createTermsBulk($params)); } elseif ($frag1 === 'multi') { return Response::success($this->multiWordHandler->createMultiWordTerm($params)); } - return Response::error('Term ID (Integer), "new", "quick", or "multi" Expected', 404); + return Response::error('Term ID (Integer), "new", "quick", "full", "bulk", or "multi" Expected', 404); } public function routePut(array $fragments, array $params): JsonResponse diff --git a/src/Modules/Vocabulary/Views/all_wellknown_result.php b/src/Modules/Vocabulary/Views/all_wellknown_result.php deleted file mode 100644 index 2ed133cab..000000000 --- a/src/Modules/Vocabulary/Views/all_wellknown_result.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - */ - -declare(strict_types=1); - -namespace Lwt\Views\Word; - -// Type assertions for variables passed from controller -assert(is_int($status)); -assert(is_int($count)); -assert(is_int($textId)); -assert(is_array($wordsData)); -assert(is_bool($useTooltips)); -assert(is_string($todoContent)); - -?> -

- 1) { - echo __('vocabulary.result.ignored_all', ['count' => $count]); - } elseif ($count == 1) { - echo __('vocabulary.result.ignored_one'); - } else { - echo __('vocabulary.result.ignored_none'); - } -} else { - if ($count > 1) { - echo __('vocabulary.result.know_all', ['count' => $count]); - } elseif ($count == 1) { - echo __('vocabulary.result.know_one'); - } else { - echo __('vocabulary.result.know_none'); - } -} -?> -

- - diff --git a/src/Modules/Vocabulary/Views/bulk_save_result.php b/src/Modules/Vocabulary/Views/bulk_save_result.php deleted file mode 100644 index e75d64eaa..000000000 --- a/src/Modules/Vocabulary/Views/bulk_save_result.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @license Unlicense - * @link https://hugofara.github.io/lwt/developer/api - * @since 3.0.0 - * - */ - -declare(strict_types=1); - -namespace Lwt\Views\Word; - -use Lwt\Shared\Infrastructure\Database\Escaping; -use Lwt\Shared\UI\Helpers\IconHelper; - -// Type assertions for variables passed from controller -assert(is_int($tid)); -assert(is_bool($cleanUp)); -assert(is_int($tooltipMode)); -assert(is_array($newWords)); -assert(is_string($todoContent)); - -?> - -

- 'icon-spin', 'alt' => $loadingAlt]); ?> - -

- - diff --git a/src/Modules/Vocabulary/Views/bulk_translate_form.php b/src/Modules/Vocabulary/Views/bulk_translate_form.php index f216a8bff..639650692 100644 --- a/src/Modules/Vocabulary/Views/bulk_translate_form.php +++ b/src/Modules/Vocabulary/Views/bulk_translate_form.php @@ -51,8 +51,22 @@ -
+ + + + + + @@ -109,7 +123,8 @@ class="button is-outlined"
-