diff --git a/index.html b/index.html
index e98fe98..40c1a7d 100644
--- a/index.html
+++ b/index.html
@@ -657,6 +657,20 @@
+
+
+ ㊙
+ Romaji Korrekturen
+
+
Kanji → korrektes Romaji. Wird in Sort-Tags automatisch übernommen.
+
+
+
+ →
+
+
+
+
🎨
diff --git a/src/api.js b/src/api.js
index c055a84..838134a 100644
--- a/src/api.js
+++ b/src/api.js
@@ -98,6 +98,22 @@ export async function fetchRomajiIndex() {
export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; _artistRomajiMap = null; }
+// ─── ROMAJI OVERRIDES ───
+// Manual kanji → romaji corrections stored in localStorage. Always take priority over auto-conversion.
+export function getRomajiOverrides() {
+ try { return JSON.parse(localStorage.getItem('ns_romaji_overrides') || '{}'); } catch { return {}; }
+}
+export function setRomajiOverride(kanji, romaji) {
+ const o = getRomajiOverrides();
+ if (romaji?.trim()) o[kanji.trim()] = romaji.trim(); else delete o[kanji.trim()];
+ localStorage.setItem('ns_romaji_overrides', JSON.stringify(o));
+}
+export function removeRomajiOverride(kanji) {
+ const o = getRomajiOverrides();
+ delete o[kanji];
+ localStorage.setItem('ns_romaji_overrides', JSON.stringify(o));
+}
+
export function dispArtistById(id, name) {
if (!S.cfg.showRomaji || !_artistRomajiMap) return name || '';
return _artistRomajiMap.get(String(id)) || name || '';
diff --git a/src/main.js b/src/main.js
index 41691b3..db1393b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -46,6 +46,7 @@ import {
toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate,
_pickTheme,
toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, deleteSong,
+ addRomajiOverrideUI, removeRomajiOverrideUI,
} from './ui.js';
// ─── CONNECT ───
@@ -323,6 +324,7 @@ Object.assign(window, {
editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel,
toggleMobileSearch, closeMobileSearch, switchSetupTab,
toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, _pickTheme,
+ addRomajiOverrideUI, removeRomajiOverrideUI,
// App
doConnect, disconnect,
// Audiobookshelf
diff --git a/src/ui.js b/src/ui.js
index 718c8c4..96da557 100644
--- a/src/ui.js
+++ b/src/ui.js
@@ -1,6 +1,6 @@
import { S } from './state.js';
import { esc, formatDur, toast } from './utils.js';
-import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache } from './api.js';
+import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache, getRomajiOverrides, setRomajiOverride, removeRomajiOverride } from './api.js';
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
// Circular imports — safe (only used inside function bodies)
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode, updateNpColorMode } from './player.js';
@@ -51,6 +51,7 @@ export function openSettings() {
document.getElementById('s-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
_renderThemePicker();
+ _renderRomajiOverrides();
document.getElementById('settings-modal').classList.add('open');
checkBackend();
}
@@ -70,6 +71,38 @@ export function _pickTheme(id) {
applyTheme(id);
_renderThemePicker();
}
+
+function _renderRomajiOverrides() {
+ const list = document.getElementById('romaji-overrides-list');
+ if (!list) return;
+ const entries = Object.entries(getRomajiOverrides());
+ if (!entries.length) {
+ list.innerHTML = '
Keine Korrekturen vorhanden
';
+ return;
+ }
+ list.innerHTML = entries.map(([k, v]) =>
+ `
+ ${esc(k)}
+ →
+ ${esc(v)}
+
+
`
+ ).join('');
+}
+export function addRomajiOverrideUI() {
+ const k = (document.getElementById('ro-kanji')?.value || '').trim();
+ const v = (document.getElementById('ro-romaji')?.value || '').trim();
+ if (!k || !v) { toast('⚠ Kanji und Romaji eingeben'); return; }
+ setRomajiOverride(k, v);
+ document.getElementById('ro-kanji').value = '';
+ document.getElementById('ro-romaji').value = '';
+ _renderRomajiOverrides();
+}
+export function removeRomajiOverrideUI(kanji) {
+ removeRomajiOverride(kanji);
+ _renderRomajiOverrides();
+}
+
export function closeSettings() { document.getElementById('settings-modal').classList.remove('open'); }
export async function confirmDisconnect() {
const ok = await showConfirm('Abmelden', 'Wirklich abmelden? Alle lokalen Einstellungen werden gelöscht.', 'Abmelden');
@@ -369,14 +402,21 @@ export function toggleInfoEdit() {
const p = new URLSearchParams({ ndUrl: S.cfg.url, user: S.cfg.user, pass: S.cfg.pass, songId: _sId });
return fetch(`${S.cfg.backendUrl}/api/song-sorttags?${p}`).then(r => r.ok ? r.json() : null).catch(() => null);
}).then(t => {
- if (!t) return;
- const _jp = /[ぁ-んァ-ン一-龯]/;
- // Only use file sort tags if they are non-Japanese (romaji/ASCII) — Japanese sort tags
- // in the file are incorrect and should be ignored in favour of the romaji index value.
- if (t.titleSort && !_jp.test(t.titleSort)) document.getElementById('ie-titlesort').value = t.titleSort;
- if (t.artistSort && !_jp.test(t.artistSort)) document.getElementById('ie-artistsort').value = t.artistSort;
- if (t.albumArtistSort && !_jp.test(t.albumArtistSort)) document.getElementById('ie-albumartistsort').value = t.albumArtistSort;
- if (t.albumSort && !_jp.test(t.albumSort)) document.getElementById('ie-albumsort').value = t.albumSort;
+ if (t) {
+ const _jp = /[ぁ-んァ-ン一-龯]/;
+ // Only use file sort tags if they are non-Japanese (romaji/ASCII) — Japanese sort tags
+ // in the file are incorrect and should be ignored in favour of the romaji index value.
+ if (t.titleSort && !_jp.test(t.titleSort)) document.getElementById('ie-titlesort').value = t.titleSort;
+ if (t.artistSort && !_jp.test(t.artistSort)) document.getElementById('ie-artistsort').value = t.artistSort;
+ if (t.albumArtistSort && !_jp.test(t.albumArtistSort)) document.getElementById('ie-albumartistsort').value = t.albumArtistSort;
+ if (t.albumSort && !_jp.test(t.albumSort)) document.getElementById('ie-albumsort').value = t.albumSort;
+ }
+ // Manual overrides — highest priority, always applied last.
+ const ov = getRomajiOverrides();
+ if (s.artist && ov[s.artist]) { document.getElementById('ie-artistsort').value = ov[s.artist]; document.getElementById('ie-albumartistsort').value = ov[s.artist]; }
+ if (s.albumArtist && ov[s.albumArtist]) document.getElementById('ie-albumartistsort').value = ov[s.albumArtist];
+ if (s.title && ov[s.title]) document.getElementById('ie-titlesort').value = ov[s.title];
+ if (s.album && ov[s.album]) document.getElementById('ie-albumsort').value = ov[s.album];
}).catch(() => {});
// Art preview — use existing cover