Replaces the read-only 'NFO Diagnostics' page with a full per-anime
Settings page reached from the right-click context menu on series cards.
Users can now view and edit key, name, folder, tmdb_id, tvdb_id and site
for each anime; changes are persisted to the DB and optionally written
back to the NFO file or used to regenerate it.
Backend
- Rename NfoDiagnosticsResponse -> NfoSettingsResponse,
NfoSeriesDiagnostics -> NfoSeriesSettings
- Rename get_nfo_diagnostics -> get_nfo_settings,
repair_nfo -> repair_nfo_settings
- Fix nfo.py bug: repair was calling non-existent
update_series_nfo_status(); now uses update_nfo_status() and an
explicit AnimeSeriesService.update(nfo_path=...)
- New endpoints on /api/anime/{key}:
GET /settings -> AnimeSettingsResponse
PUT /settings -> AnimeSettingsResponse
(body: name/folder/tmdb_id/tvdb_id/site,
options: apply_to_nfo, rename_disk)
POST /regenerate-nfo -> AnimeSettingsRegenerateNfoResponse
- New Pydantic models: AnimeSettingsResponse,
AnimeSettingsUpdateRequest, AnimeSettingsRegenerateNfoResponse
- /anime/settings page route; /settings/nfo now 301-redirects to it
Frontend
- New AniWorld.AnimeSettingsManager JS module (single-page form,
no tabs) with public API init/loadSeries/saveSettings/regenerateNfo/
validateField/populateForm/showSaveSuccess/showError
- New anime-settings.html template + anime-settings.css
- Right-click menu: data-action 'nfo-diagnostics' replaced by
'anime-settings' (label 'Anime Settings'), navigates to
/anime/settings?key=...
- Library 'Open NFO Diagnostics' link renamed to 'Open Anime Settings'
Bug fix
- context-menu click handler was calling hide() BEFORE building the
navigation URL, which cleared currentSeriesKey to null and produced
/anime/settings?key=null. Captures the key into a local const first.
Regression-locked by tests/frontend/unit/context_menu.test.js.
Tests
- 21 new pytest tests in tests/api/test_anime_settings_endpoints.py
(GET/PUT/regenerate-nfo, auth, validation, nfo-repair bug regression)
- tests/api/test_nfo_endpoints.py trimmed to 6 focused tests
- 31 new Vitest unit tests for AnimeSettingsManager
- 5 new Vitest unit tests for ContextMenu (incl. source-invariant
regression guard for the hide()-before-key bug)
- 5 new Playwright E2E tests covering right-click, direct nav,
legacy /settings/nfo redirect, and context-menu labels
- New vitest.config.js (environment: happy-dom)
Docs
- Docs/API.md: new section 'Anime Settings Endpoints'
- Docs/CHANGELOG.md: documents the rename and the context-menu bug fix
Verified
- pytest: 27/27 (21 new + 6 trimmed nfo)
- vitest: 36/36 (31 anime-settings + 5 context-menu)
- playwright e2e: 5/5
126 lines
3.6 KiB
JavaScript
126 lines
3.6 KiB
JavaScript
/**
|
|
* AniWorld - Context Menu Component
|
|
*
|
|
* Right-click context menu for anime series cards.
|
|
* Provides quick access to per-anime settings.
|
|
*
|
|
* Dependencies: ui-utils.js
|
|
*/
|
|
|
|
var AniWorld = window.AniWorld || {};
|
|
|
|
AniWorld.ContextMenu = (function() {
|
|
'use strict';
|
|
|
|
let menuElement = null;
|
|
let currentSeriesKey = null;
|
|
|
|
/**
|
|
* Initialize the context menu system.
|
|
* Attaches global dismissal listeners.
|
|
*/
|
|
function init() {
|
|
// Dismiss on click outside
|
|
document.addEventListener('click', function(e) {
|
|
if (menuElement && !menuElement.contains(e.target)) {
|
|
hide();
|
|
}
|
|
});
|
|
|
|
// Dismiss on Escape
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
hide();
|
|
}
|
|
});
|
|
|
|
// Dismiss on scroll or resize
|
|
window.addEventListener('scroll', hide, true);
|
|
window.addEventListener('resize', hide);
|
|
|
|
// Attach context menu via event delegation on the series grid
|
|
const grid = document.getElementById('series-grid');
|
|
if (grid) {
|
|
grid.addEventListener('contextmenu', function(e) {
|
|
const card = e.target.closest('.series-card');
|
|
if (card) {
|
|
e.preventDefault();
|
|
const key = card.getAttribute('data-key');
|
|
if (key) {
|
|
show(e, key);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show context menu at cursor position.
|
|
* @param {MouseEvent} event - The contextmenu event
|
|
* @param {string} seriesKey - The series key to operate on
|
|
*/
|
|
function show(event, seriesKey) {
|
|
hide(); // Remove any existing menu first
|
|
|
|
currentSeriesKey = seriesKey;
|
|
|
|
menuElement = document.createElement('div');
|
|
menuElement.className = 'context-menu';
|
|
menuElement.innerHTML = `
|
|
<div class="context-menu-item" data-action="anime-settings">
|
|
<i class="fa-solid fa-gear"></i>
|
|
<span>Anime Settings</span>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(menuElement);
|
|
|
|
// Position within viewport bounds
|
|
const x = event.clientX;
|
|
const y = event.clientY;
|
|
const menuRect = menuElement.getBoundingClientRect();
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
let posX = x;
|
|
let posY = y;
|
|
|
|
if (x + menuRect.width > viewportWidth) {
|
|
posX = viewportWidth - menuRect.width - 8;
|
|
}
|
|
if (y + menuRect.height > viewportHeight) {
|
|
posY = viewportHeight - menuRect.height - 8;
|
|
}
|
|
|
|
menuElement.style.left = posX + 'px';
|
|
menuElement.style.top = posY + 'px';
|
|
|
|
// Attach action handlers
|
|
// Anime Settings - opens the per-anime settings page
|
|
menuElement.querySelector('[data-action="anime-settings"]').addEventListener('click', function() {
|
|
// Capture the key BEFORE hide() clears it
|
|
const key = currentSeriesKey;
|
|
hide();
|
|
// Navigate to anime settings page with this series selected
|
|
window.location.href = '/anime/settings?key=' + encodeURIComponent(key);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hide and remove the context menu from DOM.
|
|
*/
|
|
function hide() {
|
|
if (menuElement) {
|
|
menuElement.remove();
|
|
menuElement = null;
|
|
currentSeriesKey = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
init: init,
|
|
show: show,
|
|
hide: hide
|
|
};
|
|
})();
|