Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CustomApps/lyrics-plus/OptionsMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmat
none: "None",
};

let romanizationOptions = {};

const musixmatchDisplay = new Intl.DisplayNames(["en"], { type: "language" });
const availableMusixmatchLanguages = Array.isArray(musixmatchLanguages) ? [...new Set(musixmatchLanguages.filter(Boolean))] : [];
const activeMusixmatchLanguage = musixmatchSelectedLanguage && musixmatchSelectedLanguage !== "none" ? musixmatchSelectedLanguage : null;
Expand Down Expand Up @@ -205,6 +207,10 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmat
hk: "Traditional Chinese (Hong Kong)",
tw: "Traditional Chinese (Taiwan)",
};
romanizationOptions = {
none: "None",
pinyin: "Pinyin (拼音)",
};
break;
}
}
Expand Down Expand Up @@ -242,6 +248,15 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmat
// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.
when: () => friendlyLanguage,
},
{
desc: "Romanization",
key: "romanization",
type: ConfigSelection,
options: romanizationOptions,
renderInline: true,
// only Chinese has a romanization option (Pinyin) for now.
when: () => friendlyLanguage === "chinese",
},
{
desc: "Convert",
key: "translate",
Expand Down Expand Up @@ -379,6 +394,14 @@ const AdjustmentsMenu = react.memo(({ mode, hasPerformer }) => {
max: fontSizeLimit.max,
step: fontSizeLimit.step,
},
{
desc: "Romanization font size",
key: "romanization-font-size",
type: ConfigAdjust,
min: fontSizeLimit.min,
max: fontSizeLimit.max,
step: fontSizeLimit.step,
},
{
desc: "Track delay",
key: "delay",
Expand Down
67 changes: 64 additions & 3 deletions CustomApps/lyrics-plus/Pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const KaraokeLine = ({ text, isActive, position, startTime, endTime }) => {
});
};

const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara }) => {
const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara, romanization }) => {
const [position, setPosition] = useState(0);
const activeLineEle = useRef();
const lyricContainerEle = useRef();
Expand All @@ -183,6 +183,17 @@ const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara
}
});

const romanizationByTime = useMemo(() => {
if (!romanization) return null;
const map = {};
for (const line of romanization) {
if (line.startTime != null) {
map[line.startTime] = line.text;
}
}
return map;
}, [romanization]);

const lyricWithEmptyLines = useMemo(
() =>
[emptyLine, emptyLine, ...processPauseLines(lyrics)].map((line, i) => ({
Expand Down Expand Up @@ -319,6 +330,10 @@ const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara

const belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;

const romanizedText = CONFIG.visual["romanization"] !== "none" && !isPause ? romanizationByTime?.[startTime] : null;
const lineTextStr = (typeof lineText === "object" ? lineText?.props?.children?.[0] : lineText) ?? "";
const showRomanizedLine = romanizedText && romanizedText.replace(/\s+/g, "") !== String(lineTextStr).replace(/\s+/g, "");

return react.createElement(
"div",
{
Expand Down Expand Up @@ -368,6 +383,17 @@ const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara
},
},
text
),
showRomanizedLine &&
react.createElement(
"p",
{
style: {
opacity: 0.5,
fontSize: "var(--lyrics-romanization-font-size)",
},
},
romanizedText
)
);
})
Expand Down Expand Up @@ -536,11 +562,22 @@ function isInViewport(element) {
);
}

const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKara }) => {
const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKara, romanization }) => {
const [position, setPosition] = useState(() => Spicetify.Player.getProgress() + CONFIG.visual["global-delay"] + CONFIG.visual.delay);
const activeLineRef = useRef(null);
const pageRef = useRef(null);

const romanizationByTime = useMemo(() => {
if (!romanization) return null;
const map = {};
for (const line of romanization) {
if (line.startTime != null) {
map[line.startTime] = line.text;
}
}
return map;
}, [romanization]);

useTrackPosition(() => {
if (!Spicetify.Player.data.is_paused) {
setPosition(Spicetify.Player.getProgress() + CONFIG.visual["global-delay"] + CONFIG.visual.delay);
Expand Down Expand Up @@ -648,6 +685,10 @@ const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKa

const belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;

const romanizedText = CONFIG.visual["romanization"] !== "none" && !isPause ? romanizationByTime?.[startTime] : null;
const lineTextStr = (typeof lineText === "object" ? lineText?.props?.children?.[0] : lineText) ?? "";
const showRomanizedLine = romanizedText && romanizedText.replace(/\s+/g, "") !== String(lineTextStr).replace(/\s+/g, "");

return react.createElement(
"div",
{
Expand Down Expand Up @@ -692,6 +733,14 @@ const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKa
},
},
text
),
showRomanizedLine &&
react.createElement(
"p",
{
style: { opacity: 0.5, fontSize: "var(--lyrics-romanization-font-size)" },
},
romanizedText
)
);
}),
Expand All @@ -706,7 +755,7 @@ const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKa
);
});

const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {
const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright, romanization }) => {
return react.createElement(
"div",
{
Expand All @@ -732,6 +781,10 @@ const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {

const belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;

const romanizedText = CONFIG.visual["romanization"] !== "none" ? romanization?.[index]?.text : null;
const lineTextStr = (typeof lineText === "object" ? lineText?.props?.children?.[0] : lineText) ?? "";
const showRomanizedLine = romanizedText && romanizedText.replace(/\s+/g, "") !== String(lineTextStr).replace(/\s+/g, "");

return react.createElement(
"div",
{
Expand Down Expand Up @@ -765,6 +818,14 @@ const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {
},
},
text
),
showRomanizedLine &&
react.createElement(
"p",
{
style: { opacity: 0.5, fontSize: "var(--lyrics-romanization-font-size)" },
},
romanizedText
)
);
}),
Expand Down
34 changes: 28 additions & 6 deletions CustomApps/lyrics-plus/Translator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const kuroshiroPath = "https://cdn.jsdelivr.net/npm/kuroshiro@1.2.0/dist/kuroshi
const kuromojiPath = "https://cdn.jsdelivr.net/npm/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js";
const aromanize = "https://cdn.jsdelivr.net/npm/aromanize@0.1.5/aromanize.min.js";
const openCCPath = "https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/dist/umd/full.min.js";
const pinyinProPath = "https://cdn.jsdelivr.net/npm/pinyin-pro@3.28.1/dist/index.min.js";

const dictPath = "https:/cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict";

Expand All @@ -19,15 +20,19 @@ class Translator {
this.createTranslator(lang);
}

includeExternal(url) {
if ((CONFIG.visual.translate || this.isUsingNetease) && !document.querySelector(`script[src="${url}"]`)) {
includeExternal(url, forceInclude = false) {
if ((forceInclude || CONFIG.visual.translate || this.isUsingNetease) && !document.querySelector(`script[src="${url}"]`)) {
const script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("src", url);
document.head.appendChild(script);
}
}

isUsingRomanization() {
return CONFIG.visual["romanization"] && CONFIG.visual["romanization"] !== "none";
}

injectExternals(lang) {
switch (lang?.slice(0, 2)) {
case "ja":
Expand All @@ -39,6 +44,7 @@ class Translator {
break;
case "zh":
this.includeExternal(openCCPath);
this.includeExternal(pinyinProPath, this.isUsingRomanization());
break;
}
}
Expand Down Expand Up @@ -101,16 +107,23 @@ class Translator {
this.Aromanize = Aromanize;
this.finished.ko = true;
break;
case "zh":
if (this.OpenCC) return;
if (typeof OpenCC === "undefined") {
case "zh": {
const needsOpenCC = CONFIG.visual.translate || this.isUsingNetease;
const needsPinyin = this.isUsingRomanization();
if (needsOpenCC && typeof OpenCC === "undefined") {
await Translator.#sleep(50);
return this.createTranslator(lang);
}
if (needsPinyin && typeof pinyinPro === "undefined") {
await Translator.#sleep(50);
return this.createTranslator(lang);
}

this.OpenCC = OpenCC;
if (needsOpenCC && !this.OpenCC) this.OpenCC = OpenCC;
if (needsPinyin && !this.pinyinPro) this.pinyinPro = pinyinPro;
this.finished.zh = true;
break;
}
}
}

Expand Down Expand Up @@ -150,6 +163,15 @@ class Translator {
return converter(text);
}

async convertToPinyin(text) {
if (!this.finished.zh || !this.pinyinPro) {
await Translator.#sleep(100);
return this.convertToPinyin(text);
}

return this.pinyinPro.pinyin(text, { toneType: "symbol", nonZh: "consecutive" });
}

/**
* Async wrapper of `setTimeout`.
*
Expand Down
Loading