Files
Aniworld/src/server/web/static/js/index/context-menu.js
Lukas eabce18e41 feat(anime): rename NFO Diagnostics to Anime Settings + add edit endpoints
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
2026-06-21 07:52:22 +02:00

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
};
})();