Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ ones are marked like "v1.0.0-fork".

### Fixed

* **The "edit term" button in the review table went nowhere** (#266): it pointed
at `edit_tword.php`, a filename that stopped being routed in v3, and opened it
in a frame the reader no longer renders. It now links to `/word/edit-term`,
the same route the Alpine review view already used.

* **A single-language install could never browse Gutenberg or GDL**: the
new-text page said "Please select a language above" while the navbar already
showed one. `currentlanguage` is only written when the user picks from the
Expand Down Expand Up @@ -126,6 +131,16 @@ ones are marked like "v1.0.0-fork".
PHP 8.5 surfaced by deprecating `ord()` on multi-byte strings. No API or
styling change.

### Removed

* **Dead legacy result-view plumbing** (#266): four result handlers
(`delete_result`, `insert_wellknown_result`, `insert_ignore_result`,
`delete_multi_result`) outlived the views that once fed them, and
`word_status_ajax.ts` waited on a `#word-status-config` element no page has
emitted since the frame reader was retired. Dropping them plus their now
orphaned DOM helpers removes ~1000 lines. No behaviour change — none of it
could run.

## [3.2.1-fork] - 2026-06-30

### Fixed
Expand Down
52 changes: 36 additions & 16 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pay for the hard parts when demand proves them worth it.

## The keystone constraint

LWT renders HTML server-side (94 view templates). Until more logic moves to the
LWT renders HTML server-side (93 view templates). Until more logic moves to the
frontend against `/api/v1`, *any* mobile wrapper is just a window to a server.
So **"more frontend, less server-rendered" is the enabling work** — the thing
that makes a good client today and offline/local-first possible tomorrow.
Expand Down Expand Up @@ -93,7 +93,12 @@ that makes a good client today and offline/local-first possible tomorrow.
`textReader.renderTextContent()` against `/api/v1`, with an offline prototype
(`offline-text-reader.ts` + IndexedDB) started. `review_desktop.php` is "all UI
rendered by Alpine.js." So Phase 1 is **cutting the server-shell umbilical, not
converting pages**. Three dependencies to sever + one real conversion:
converting pages**.

**Status (re-audited 2026-08-05): every mobile-critical surface is shell-free.**
The only open item is the legacy-fragment cleanup in the last bullet, which is
not a mobile blocker. Tracked on the issue board as
[#266](https://github.com/HugoFara/lwt/issues/266).

- [x] **(Phase 0 gate) Injectable API base URL.** Done in Phase 0 — same seam.
`@shared/api/client` resolves an injectable **absolute** server root and
Expand All @@ -109,9 +114,18 @@ converting pages**. Three dependencies to sever + one real conversion:
`audioPlayer` fetch), so `read_desktop.php` carries no per-text data and
`TextReadController` dropped the server-side book/media plumbing. Verified
live (book nav, audio reveal, no-regression plain read). Toolbar labels use
`__e()` but resolve via the client i18n `t()`. *Remaining (lower urgency,
not reader-specific):* the **global navbar** is still PHP-rendered — tracked
separately since it spans every page.
`__e()` but resolve via the client i18n `t()`. Nothing outstanding — the
global navbar was the last shared dependency and it shipped (next bullet).
- [x] **Global navbar — shell-free.** Shipped in 3.2.0 (`d6e6a5203`).
`PageLayoutHelper::buildNavbar()` is gone; PHP now emits only the mount
point `<div id="navbar-root" data-current-page="…">`
(`buildNavbarPlaceholder()`), and `mountNavbar()` fetches
`GET /api/v1/navbar` (per-user language list, current language, theme
state, admin/multi-user flags) and builds the markup in
`shared/components/navbar_renderer.ts`. Labels resolve from the i18n
bundle, so the payload stays locale-agnostic. Zero server-rendered navbar
markup remains in `src/**/*.php`. This was the last chrome dependency
spanning every page.
- [x] **i18n → client (delivery mechanism).** Shipped: `GET /api/v1/i18n[/{locale}]`
(public; `Translator::getAllTranslations()`) returns the flat
"namespace.key" => string bundle, merging English fallback — the same shape
Expand All @@ -135,8 +149,8 @@ converting pages**. Three dependencies to sever + one real conversion:
and the destructive **bulk archive/delete** now go through
`PUT /api/v1/texts/bulk-action` (per-user scoped) instead of a same-origin
form POST, so they work against a configurable API base. The remaining
bulk actions (tag / review / reparse) intentionally stay on the form path —
they need pickers/navigation and are desktop-admin, not mobile flows.
bulk actions (tag / review / reparse) intentionally stay on the form
path — they need pickers/navigation and are desktop-admin, not mobile.
`__e()` labels resolve via the i18n API once a page boots from it.
- [~] **Vocabulary mgmt — mobile path already shell-free; legacy fragments
remain.** *Re-audit (corrected):* the **modern reader's** word actions
Expand All @@ -153,6 +167,10 @@ converting pages**. Three dependencies to sever + one real conversion:
those popups onto the API button family / `word_modal`** and then deleting
the fragments — a UI-consolidation pass that needs live E2E in the reader,
not a server-vs-client data conversion. Track as cleanup; low urgency.
Still open as of 2026-08-05: eight `*_result.php` views survive
(`save_result`, `edit_result`, `edit_term_result`, `edit_multi_update_result`,
`hover_save_result`, `all_wellknown_result`, `bulk_save_result`,
`upload_result`) plus `Book/Views/import_result.php`.

**Out of Phase 1** (leave server-rendered, fine in a WebView online): imports
(file/web/youtube/whisper), admin/settings, language config, feeds.
Expand All @@ -176,15 +194,17 @@ the next achievable target. See `lukaisu/ROADMAP.md` for the build-side detail.
persisted in native Preferences, then navigates the WebView to the server.
- [~] **Bundled-client (Model B) build mode.** `npm run build:app`
(`vite.app.config.ts` → `dist-app/`) emits standalone connect/library/
reader pages that boot `main.ts` against a remote `/api/v1` with no PHP in
the loop. Each page body is **prerendered from the real PHP view** at build
time (`build/php-view-prerender.mjs`) so the Alpine scaffolds, icons and
labels stay a single source of truth. `../lukaisu` consumes `dist-app/` as
its Capacitor `webDir` (`npm run sync:model-b` there). First slice: connect
→ library → reader. **CORS is now required** — the bundle origin
(`https://localhost`) is cross-origin to every server, so servers must set
`CORS_ALLOWED_ORIGINS=https://localhost`. Remaining: review surface,
client-rendered global navbar, on-device QA.
reader/review pages that boot `main.ts` against a remote `/api/v1` with no
PHP in the loop. Each page body is **prerendered from the real PHP view**
at build time (`build/php-view-prerender.mjs`) so the Alpine scaffolds,
icons and labels stay a single source of truth. `../lukaisu` consumes
`dist-app/` as
its Capacitor `webDir` (`npm run sync:model-b` there). **CORS is now
required** — the bundle origin (`https://localhost`) is cross-origin to
every server, so servers must set `CORS_ALLOWED_ORIGINS=https://localhost`.
All four surfaces are bundled: connect → library → reader → **review**
(`f1f062c24`), and the navbar they share is client-rendered too.
**Remaining: on-device QA only** — everything else in this bullet ships.
- [ ] Reuse existing PWA assets; close the small gaps: real adaptive/maskable
launcher icons (replace placeholders) and a manifest **`id`** (Lukaisu v0.2).
- [ ] Ship through **our own F-Droid repo first** (low bar, full control,
Expand Down
3 changes: 1 addition & 2 deletions src/Modules/Review/Views/table_review_row.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@
?>
<tr>
<td class="has-text-centered" nowrap="nowrap">
<a href="edit_tword.php?wid=<?php echo $woId; ?>" target="ro"
data-action="show-right-frames">
<a href="/word/edit-term?wid=<?php echo $woId; ?>">
<?php $editTermLabel = __('review.table.edit_term'); ?>
<?php echo IconHelper::render(
'file-pen-line',
Expand Down
1 change: 0 additions & 1 deletion src/frontend/js/modules/vocabulary/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export * from './services/dictionary';
export * from './services/translation_api';
export * from './services/term_operations';
export * from './services/word_dom_updates';
export * from './services/word_status_ajax';

// Shared utilities needed by vocabulary pages
import '@shared/forms/bulk_actions';
Expand Down
138 changes: 14 additions & 124 deletions src/frontend/js/modules/vocabulary/pages/word_result_init.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
/**
* Word Result Initialization - Auto-initializes word result views.
*
* Handles initialization of result views after word operations:
* - save_result.php (new word saved)
* Handles initialization of result views after word operations. Each view emits a
* `<script type="application/json" data-lwt-*-config>` blob that the matching
* initializer below reads and applies to the reading frame's DOM. The full set of
* views still using this mechanism:
*
* - save_result.php (new word saved in multi-word context)
* - edit_result.php (word created or updated)
* - edit_term_result.php (word updated during review)
* - edit_multi_update_result.php (multi-word expression updated)
* - hover_save_result.php (word saved via hover)
* - all_wellknown_result.php (mark all as well-known/ignored)
* - bulk_save_result.php (bulk translated words saved)
*
* Keep this list in sync with the views — a handler with no emitting view is dead
* code, which is how the delete/insert-wellknown/insert-ignore/delete-multi
* handlers came to be removed.
*
* @license Unlicense <http://unlicense.org/>
* @since 3.0.0
Expand All @@ -18,11 +30,7 @@ import {
getParentContext,
updateLearnStatus,
updateTestWordInDOM,
deleteWordFromDOM,
markWordWellKnownInDOM,
markWordIgnoredInDOM,
updateMultiWordInDOM,
deleteMultiWordFromDOM,
updateBulkWordInDOM,
type BulkWordUpdateParams
} from '../services/word_dom_updates';
Expand Down Expand Up @@ -109,35 +117,6 @@ interface HoverSaveResultConfig {
todoContent: string;
}

/**
* Configuration for delete_result view (word deleted).
*/
interface DeleteResultConfig {
wid: number;
term: string;
todoContent: string;
}

/**
* Configuration for insert_wellknown_result view (word marked as well-known).
*/
interface InsertWellKnownResultConfig {
wid: number;
hex: string;
term: string;
todoContent: string;
}

/**
* Configuration for insert_ignore_result view (word marked as ignored).
*/
interface InsertIgnoreResultConfig {
wid: number;
hex: string;
term: string;
todoContent: string;
}

/**
* Configuration for edit_multi_update_result view (multi-word expression updated).
*/
Expand All @@ -150,15 +129,6 @@ interface EditMultiUpdateResultConfig {
oldStatus: number;
}

/**
* Configuration for delete_multi_result view (multi-word expression deleted).
*/
interface DeleteMultiResultConfig {
wid: number;
showAll: boolean;
todoContent: string;
}

/**
* Configuration for bulk_save_result view (bulk translated words saved).
*/
Expand Down Expand Up @@ -320,33 +290,6 @@ function initEditResult(config: EditResultConfig): void {
completeWordOperation(config.todoContent);
}

/**
* Initialize delete_result view.
* Updates the DOM after a word is deleted (reset to unknown state).
*/
function initDeleteResult(config: DeleteResultConfig): void {
deleteWordFromDOM(config.wid, config.term);
completeWordOperation(config.todoContent);
}

/**
* Initialize insert_wellknown_result view.
* Updates the DOM after marking a word as well-known (status 99).
*/
function initInsertWellKnownResult(config: InsertWellKnownResultConfig): void {
markWordWellKnownInDOM(config.wid, config.hex, config.term);
completeWordOperation(config.todoContent);
}

/**
* Initialize insert_ignore_result view.
* Updates the DOM after marking a word as ignored (status 98).
*/
function initInsertIgnoreResult(config: InsertIgnoreResultConfig): void {
markWordIgnoredInDOM(config.wid, config.hex, config.term);
completeWordOperation(config.todoContent);
}

/**
* Initialize edit_multi_update_result view.
* Updates the DOM after a multi-word expression is updated.
Expand All @@ -363,15 +306,6 @@ function initEditMultiUpdateResult(config: EditMultiUpdateResultConfig): void {
cleanupRightFrames();
}

/**
* Initialize delete_multi_result view.
* Updates the DOM after a multi-word expression is deleted.
*/
function initDeleteMultiResult(config: DeleteMultiResultConfig): void {
deleteMultiWordFromDOM(config.wid, config.showAll);
completeWordOperation(config.todoContent);
}

/**
* Initialize bulk_save_result view.
* Updates the DOM after bulk translated words are saved.
Expand Down Expand Up @@ -455,39 +389,6 @@ export function autoInitWordResults(): void {
}
}

// Delete result
const deleteConfigEl = document.querySelector<HTMLScriptElement>('script[data-lwt-delete-result-config]');
if (deleteConfigEl) {
try {
const config = JSON.parse(deleteConfigEl.textContent || '{}') as DeleteResultConfig;
initDeleteResult(config);
} catch (e) {
console.error('Failed to parse delete result config:', e);
}
}

// Insert well-known result
const insertWellKnownConfigEl = document.querySelector<HTMLScriptElement>('script[data-lwt-insert-wellknown-result-config]');
if (insertWellKnownConfigEl) {
try {
const config = JSON.parse(insertWellKnownConfigEl.textContent || '{}') as InsertWellKnownResultConfig;
initInsertWellKnownResult(config);
} catch (e) {
console.error('Failed to parse insert wellknown result config:', e);
}
}

// Insert ignore result
const insertIgnoreConfigEl = document.querySelector<HTMLScriptElement>('script[data-lwt-insert-ignore-result-config]');
if (insertIgnoreConfigEl) {
try {
const config = JSON.parse(insertIgnoreConfigEl.textContent || '{}') as InsertIgnoreResultConfig;
initInsertIgnoreResult(config);
} catch (e) {
console.error('Failed to parse insert ignore result config:', e);
}
}

// Edit multi-word update result
const editMultiUpdateConfigEl = document.querySelector<HTMLScriptElement>('script[data-lwt-edit-multi-update-result-config]');
if (editMultiUpdateConfigEl) {
Expand All @@ -499,17 +400,6 @@ export function autoInitWordResults(): void {
}
}

// Delete multi-word result
const deleteMultiConfigEl = document.querySelector<HTMLScriptElement>('script[data-lwt-delete-multi-result-config]');
if (deleteMultiConfigEl) {
try {
const config = JSON.parse(deleteMultiConfigEl.textContent || '{}') as DeleteMultiResultConfig;
initDeleteMultiResult(config);
} catch (e) {
console.error('Failed to parse delete multi result config:', e);
}
}

// Bulk save result
const bulkSaveConfigEl = document.querySelector<HTMLScriptElement>('script[data-lwt-bulk-save-result-config]');
if (bulkSaveConfigEl) {
Expand Down
Loading