From 8c3a23ad1471fd5d6847f11f7a24d7641406c94e Mon Sep 17 00:00:00 2001 From: Honey Date: Thu, 4 Jun 2026 20:17:13 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20romaji=20override=20list=20=E2=80=94=20?= =?UTF-8?q?manual=20kanji=E2=86=92romaji=20corrections=20for=20sort=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- index.html | 14 +++++++++++++ src/api.js | 16 +++++++++++++++ src/main.js | 2 ++ src/ui.js | 58 ++++++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 81 insertions(+), 9 deletions(-) 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