feat: romaji override list — manual kanji→romaji corrections for sort tags

Adds a maintained list in Settings where you can map kanji artist/album/title
strings to their correct romaji reading (e.g. 照井順政 → Terui Yoshimasa).
Overrides are applied last (highest priority) when the edit-metadata modal
opens, so saving the song writes the correct sort tags permanently to the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-04 20:17:13 +02:00
1 parent db7a515ee4
commit 8c3a23ad14
4 files changed
+81 -9

No files matched your search

+14
View File
@@ -657,6 +657,20 @@
<div id="s-show-romaji" class="toggle" onclick="this.classList.toggle('on')"></div> <div id="s-show-romaji" class="toggle" onclick="this.classList.toggle('on')"></div>
</div> </div>
</div> </div>
<div class="settings-section">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<span style="width:24px;height:24px;border-radius:6px;background:var(--accent-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">㊙</span>
Romaji Korrekturen
</h3>
<div class="hint" style="margin-bottom:10px">Kanji → korrektes Romaji. Wird in Sort-Tags automatisch übernommen.</div>
<div id="romaji-overrides-list" style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px"></div>
<div style="display:flex;gap:6px;align-items:center">
<input id="ro-kanji" placeholder="照井順政" style="flex:1;padding:8px 10px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none" onkeydown="if(event.key==='Enter')addRomajiOverrideUI()">
<span style="color:var(--text2);flex-shrink:0">→</span>
<input id="ro-romaji" placeholder="Terui Yoshimasa" style="flex:1.2;padding:8px 10px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none" onkeydown="if(event.key==='Enter')addRomajiOverrideUI()">
<button onclick="addRomajiOverrideUI()" style="flex-shrink:0;padding:8px 12px;border-radius:8px;border:none;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000;font-family:var(--font);font-size:14px;font-weight:700;cursor:pointer">+</button>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px"> <h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🎨</span> <span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🎨</span>
+16
View File
@@ -98,6 +98,22 @@ export async function fetchRomajiIndex() {
export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; _artistRomajiMap = null; } 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) { export function dispArtistById(id, name) {
if (!S.cfg.showRomaji || !_artistRomajiMap) return name || ''; if (!S.cfg.showRomaji || !_artistRomajiMap) return name || '';
return _artistRomajiMap.get(String(id)) || name || ''; return _artistRomajiMap.get(String(id)) || name || '';
+2
View File
@@ -46,6 +46,7 @@ import {
toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate, toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate,
_pickTheme, _pickTheme,
toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, deleteSong, toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, deleteSong,
addRomajiOverrideUI, removeRomajiOverrideUI,
} from './ui.js'; } from './ui.js';
// ─── CONNECT ─── // ─── CONNECT ───
@@ -323,6 +324,7 @@ Object.assign(window, {
editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel,
toggleMobileSearch, closeMobileSearch, switchSetupTab, toggleMobileSearch, closeMobileSearch, switchSetupTab,
toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, _pickTheme, toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, _pickTheme,
addRomajiOverrideUI, removeRomajiOverrideUI,
// App // App
doConnect, disconnect, doConnect, disconnect,
// Audiobookshelf // Audiobookshelf
+49 -9
View File
@@ -1,6 +1,6 @@
import { S } from './state.js'; import { S } from './state.js';
import { esc, formatDur, toast } from './utils.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'; import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
// Circular imports — safe (only used inside function bodies) // 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'; 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-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg); document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
_renderThemePicker(); _renderThemePicker();
_renderRomajiOverrides();
document.getElementById('settings-modal').classList.add('open'); document.getElementById('settings-modal').classList.add('open');
checkBackend(); checkBackend();
} }
@@ -70,6 +71,38 @@ export function _pickTheme(id) {
applyTheme(id); applyTheme(id);
_renderThemePicker(); _renderThemePicker();
} }
function _renderRomajiOverrides() {
const list = document.getElementById('romaji-overrides-list');
if (!list) return;
const entries = Object.entries(getRomajiOverrides());
if (!entries.length) {
list.innerHTML = '<div style="color:var(--text2);font-size:13px;padding:4px 0">Keine Korrekturen vorhanden</div>';
return;
}
list.innerHTML = entries.map(([k, v]) =>
`<div style="display:flex;align-items:center;gap:6px;padding:7px 10px;background:var(--bg2);border-radius:8px">
<span style="flex:1;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(k)}</span>
<span style="color:var(--text2);flex-shrink:0">→</span>
<span style="flex:1.2;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(v)}</span>
<button onclick="removeRomajiOverrideUI(${JSON.stringify(k)})" style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:18px;line-height:1;padding:0 2px;flex-shrink:0">×</button>
</div>`
).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 function closeSettings() { document.getElementById('settings-modal').classList.remove('open'); }
export async function confirmDisconnect() { export async function confirmDisconnect() {
const ok = await showConfirm('Abmelden', 'Wirklich abmelden? Alle lokalen Einstellungen werden gelöscht.', 'Abmelden'); 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 }); 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); return fetch(`${S.cfg.backendUrl}/api/song-sorttags?${p}`).then(r => r.ok ? r.json() : null).catch(() => null);
}).then(t => { }).then(t => {
if (!t) return; if (t) {
const _jp = /[ぁ-んァ-ン一-龯]/; const _jp = /[ぁ-んァ-ン一-龯]/;
// Only use file sort tags if they are non-Japanese (romaji/ASCII) — Japanese sort tags // 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. // 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.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.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.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.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(() => {}); }).catch(() => {});
// Art preview — use existing cover // Art preview — use existing cover