dotfiles/.config/spicetify/CustomApps/lyrics-plus/Settings.js
2024-06-26 12:05:08 +02:00

657 lines
14 KiB
JavaScript

const ButtonSVG = ({ icon, active = true, onClick }) => {
return react.createElement(
"button",
{
className: `switch${active ? "" : " disabled"}`,
onClick
},
react.createElement("svg", {
width: 16,
height: 16,
viewBox: "0 0 16 16",
fill: "currentColor",
dangerouslySetInnerHTML: {
__html: icon
}
})
);
};
const SwapButton = ({ icon, disabled, onClick }) => {
return react.createElement(
"button",
{
className: "switch small",
onClick,
disabled
},
react.createElement("svg", {
width: 10,
height: 10,
viewBox: "0 0 16 16",
fill: "currentColor",
dangerouslySetInnerHTML: {
__html: icon
}
})
);
};
const CacheButton = () => {
let lyrics = {};
try {
const localLyrics = JSON.parse(localStorage.getItem("lyrics-plus:local-lyrics"));
if (!localLyrics || typeof localLyrics !== "object") {
throw "";
}
lyrics = localLyrics;
} catch {
lyrics = {};
}
const [count, setCount] = useState(Object.keys(lyrics).length);
const text = count ? "Clear cached lyrics" : "No cached lyrics";
return react.createElement(
"button",
{
className: "btn",
onClick: () => {
localStorage.removeItem("lyrics-plus:local-lyrics");
setCount(0);
},
disabled: !count
},
text
);
};
const RefreshTokenButton = ({ setTokenCallback }) => {
const [buttonText, setButtonText] = useState("Refresh token");
useEffect(() => {
if (buttonText === "Refreshing token...") {
Spicetify.CosmosAsync.get("https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0", null, {
authority: "apic-desktop.musixmatch.com"
})
.then(({ message: response }) => {
if (response.header.status_code === 200 && response.body.user_token) {
setTokenCallback(response.body.user_token);
setButtonText("Token refreshed");
} else if (response.header.status_code === 401) {
setButtonText("Too many attempts");
} else {
setButtonText("Failed to refresh token");
console.error("Failed to refresh token", response);
}
})
.catch(error => {
setButtonText("Failed to refresh token");
console.error("Failed to refresh token", error);
});
}
}, [buttonText]);
return react.createElement(
"button",
{
className: "btn",
onClick: () => {
setButtonText("Refreshing token...");
},
disabled: buttonText !== "Refresh token"
},
buttonText
);
};
const ConfigSlider = ({ name, defaultValue, onChange = () => {} }) => {
const [active, setActive] = useState(defaultValue);
const toggleState = useCallback(() => {
const state = !active;
setActive(state);
onChange(state);
}, [active]);
return react.createElement(
"div",
{
className: "setting-row"
},
react.createElement(
"label",
{
className: "col description"
},
name
),
react.createElement(
"div",
{
className: "col action"
},
react.createElement(ButtonSVG, {
icon: Spicetify.SVGIcons.check,
active,
onClick: toggleState
})
)
);
};
const ConfigSelection = ({ name, defaultValue, options, onChange = () => {} }) => {
const [value, setValue] = useState(defaultValue);
const setValueCallback = useCallback(
event => {
let value = event.target.value;
if (!Number.isNaN(Number(value))) {
value = Number.parseInt(value);
}
setValue(value);
onChange(value);
},
[value, options]
);
useEffect(() => {
setValue(defaultValue);
}, [defaultValue]);
if (!Object.keys(options).length) return null;
return react.createElement(
"div",
{
className: "setting-row"
},
react.createElement(
"label",
{
className: "col description"
},
name
),
react.createElement(
"div",
{
className: "col action"
},
react.createElement(
"select",
{
className: "main-dropDown-dropDown",
value,
onChange: setValueCallback
},
Object.keys(options).map(item =>
react.createElement(
"option",
{
value: item
},
options[item]
)
)
)
)
);
};
const ConfigInput = ({ name, defaultValue, onChange = () => {} }) => {
const [value, setValue] = useState(defaultValue);
const setValueCallback = useCallback(
event => {
const value = event.target.value;
setValue(value);
onChange(value);
},
[value]
);
return react.createElement(
"div",
{
className: "setting-row"
},
react.createElement(
"label",
{
className: "col description"
},
name
),
react.createElement(
"div",
{
className: "col action"
},
react.createElement("input", {
value,
onChange: setValueCallback
})
)
);
};
const ConfigAdjust = ({ name, defaultValue, step, min, max, onChange = () => {} }) => {
const [value, setValue] = useState(defaultValue);
function adjust(dir) {
let temp = value + dir * step;
if (temp < min) {
temp = min;
} else if (temp > max) {
temp = max;
}
setValue(temp);
onChange(temp);
}
return react.createElement(
"div",
{
className: "setting-row"
},
react.createElement(
"label",
{
className: "col description"
},
name
),
react.createElement(
"div",
{
className: "col action"
},
react.createElement(SwapButton, {
icon: `<path d="M2 7h12v2H0z"/>`,
onClick: () => adjust(-1),
disabled: value === min
}),
react.createElement(
"p",
{
className: "adjust-value"
},
value
),
react.createElement(SwapButton, {
icon: Spicetify.SVGIcons.plus2px,
onClick: () => adjust(1),
disabled: value === max
})
)
);
};
const ConfigHotkey = ({ name, defaultValue, onChange = () => {} }) => {
const [value, setValue] = useState(defaultValue);
const [trap] = useState(new Spicetify.Mousetrap());
function record() {
trap.handleKey = (character, modifiers, e) => {
if (e.type === "keydown") {
const sequence = [...new Set([...modifiers, character])];
if (sequence.length === 1 && sequence[0] === "esc") {
onChange("");
setValue("");
return;
}
setValue(sequence.join("+"));
}
};
}
function finishRecord() {
trap.handleKey = () => {};
onChange(value);
}
return react.createElement(
"div",
{
className: "setting-row"
},
react.createElement(
"label",
{
className: "col description"
},
name
),
react.createElement(
"div",
{
className: "col action"
},
react.createElement("input", {
value,
onFocus: record,
onBlur: finishRecord
})
)
);
};
const ServiceAction = ({ item, setTokenCallback }) => {
switch (item.name) {
case "local":
return react.createElement(CacheButton);
case "musixmatch":
return react.createElement(RefreshTokenButton, { setTokenCallback });
default:
return null;
}
};
const ServiceOption = ({ item, onToggle, onSwap, isFirst = false, isLast = false, onTokenChange = null }) => {
const [token, setToken] = useState(item.token);
const [active, setActive] = useState(item.on);
const setTokenCallback = useCallback(
token => {
setToken(token);
onTokenChange(item.name, token);
},
[item.token]
);
const toggleActive = useCallback(() => {
if (item.name === "genius" && spotifyVersion >= "1.2.31") return;
const state = !active;
setActive(state);
onToggle(item.name, state);
}, [active]);
return react.createElement(
"div",
null,
react.createElement(
"div",
{
className: "setting-row"
},
react.createElement(
"h3",
{
className: "col description"
},
item.name
),
react.createElement(
"div",
{
className: "col action"
},
react.createElement(ServiceAction, {
item,
setTokenCallback
}),
react.createElement(SwapButton, {
icon: Spicetify.SVGIcons["chart-up"],
onClick: () => onSwap(item.name, -1),
disabled: isFirst
}),
react.createElement(SwapButton, {
icon: Spicetify.SVGIcons["chart-down"],
onClick: () => onSwap(item.name, 1),
disabled: isLast
}),
react.createElement(ButtonSVG, {
icon: Spicetify.SVGIcons.check,
active,
onClick: toggleActive
})
)
),
react.createElement("span", {
dangerouslySetInnerHTML: {
__html: item.desc
}
}),
item.token !== undefined &&
react.createElement("input", {
placeholder: `Place your ${item.name} token here`,
value: token,
onChange: event => setTokenCallback(event.target.value)
})
);
};
const ServiceList = ({ itemsList, onListChange = () => {}, onToggle = () => {}, onTokenChange = () => {} }) => {
const [items, setItems] = useState(itemsList);
const maxIndex = items.length - 1;
const onSwap = useCallback(
(name, direction) => {
const curPos = items.findIndex(val => val === name);
const newPos = curPos + direction;
[items[curPos], items[newPos]] = [items[newPos], items[curPos]];
onListChange(items);
setItems([...items]);
},
[items]
);
return items.map((key, index) => {
const item = CONFIG.providers[key];
item.name = key;
return react.createElement(ServiceOption, {
item,
key,
isFirst: index === 0,
isLast: index === maxIndex,
onSwap,
onTokenChange,
onToggle
});
});
};
const OptionList = ({ type, items, onChange }) => {
const [itemList, setItemList] = useState(items);
const [, forceUpdate] = useState();
useEffect(() => {
if (!type) return;
const eventListener = event => {
if (event.detail?.type !== type) return;
setItemList(event.detail.items);
};
document.addEventListener("lyrics-plus", eventListener);
return () => document.removeEventListener("lyrics-plus", eventListener);
}, []);
return itemList.map(item => {
if (!item || (item.when && !item.when())) {
return;
}
const onChangeItem = item.onChange || onChange;
return react.createElement(
"div",
null,
react.createElement(item.type, {
...item,
name: item.desc,
defaultValue: CONFIG.visual[item.key],
onChange: value => {
onChangeItem(item.key, value);
forceUpdate({});
}
}),
item.info &&
react.createElement("span", {
dangerouslySetInnerHTML: {
__html: item.info
}
})
);
});
};
function openConfig() {
const configContainer = react.createElement(
"div",
{
id: `${APP_NAME}-config-container`
},
react.createElement("h2", null, "Options"),
react.createElement(OptionList, {
items: [
{
desc: "Playbar button",
key: "playbar-button",
info: "Replace Spotify's lyrics button with Lyrics Plus.",
type: ConfigSlider
},
{
desc: "Global delay",
info: "Offset (in ms) across all tracks.",
key: "global-delay",
type: ConfigAdjust,
min: -10000,
max: 10000,
step: 250
},
{
desc: "Font size",
info: "(or Ctrl + Mouse scroll in main app)",
key: "font-size",
type: ConfigAdjust,
min: fontSizeLimit.min,
max: fontSizeLimit.max,
step: fontSizeLimit.step
},
{
desc: "Alignment",
key: "alignment",
type: ConfigSelection,
options: {
left: "Left",
center: "Center",
right: "Right"
}
},
{
desc: "Fullscreen hotkey",
key: "fullscreen-key",
type: ConfigHotkey
},
{
desc: "Compact synced: Lines to show before",
key: "lines-before",
type: ConfigSelection,
options: [0, 1, 2, 3, 4]
},
{
desc: "Compact synced: Lines to show after",
key: "lines-after",
type: ConfigSelection,
options: [0, 1, 2, 3, 4]
},
{
desc: "Compact synced: Fade-out blur",
key: "fade-blur",
type: ConfigSlider
},
{
desc: "Noise overlay",
key: "noise",
type: ConfigSlider
},
{
desc: "Colorful background",
key: "colorful",
type: ConfigSlider
},
{
desc: "Background color",
key: "background-color",
type: ConfigInput,
when: () => !CONFIG.visual.colorful
},
{
desc: "Active text color",
key: "active-color",
type: ConfigInput,
when: () => !CONFIG.visual.colorful
},
{
desc: "Inactive text color",
key: "inactive-color",
type: ConfigInput,
when: () => !CONFIG.visual.colorful
},
{
desc: "Highlight text background",
key: "highlight-color",
type: ConfigInput,
when: () => !CONFIG.visual.colorful
},
{
desc: "Text convertion: Japanese Detection threshold (Advanced)",
info: "Checks if whenever Kana is dominant in lyrics. If the result passes the threshold, it's most likely Japanese, and vice versa. This setting is in percentage.",
key: "ja-detect-threshold",
type: ConfigAdjust,
min: thresholdSizeLimit.min,
max: thresholdSizeLimit.max,
step: thresholdSizeLimit.step
},
{
desc: "Text convertion: Traditional-Simplified Detection threshold (Advanced)",
info: "Checks if whenever Traditional or Simplified is dominant in lyrics. If the result passes the threshold, it's most likely Simplified, and vice versa. This setting is in percentage.",
key: "hans-detect-threshold",
type: ConfigAdjust,
min: thresholdSizeLimit.min,
max: thresholdSizeLimit.max,
step: thresholdSizeLimit.step
}
],
onChange: (name, value) => {
CONFIG.visual[name] = value;
localStorage.setItem(`${APP_NAME}:visual:${name}`, value);
lyricContainerUpdate?.();
const configChange = new CustomEvent("lyrics-plus", {
detail: {
type: "config",
name: name,
value: value
}
});
window.dispatchEvent(configChange);
}
}),
react.createElement("h2", null, "Providers"),
react.createElement(ServiceList, {
itemsList: CONFIG.providersOrder,
onListChange: list => {
CONFIG.providersOrder = list;
localStorage.setItem(`${APP_NAME}:services-order`, JSON.stringify(list));
},
onToggle: (name, value) => {
CONFIG.providers[name].on = value;
localStorage.setItem(`${APP_NAME}:provider:${name}:on`, value);
lyricContainerUpdate?.();
},
onTokenChange: (name, value) => {
CONFIG.providers[name].token = value;
localStorage.setItem(`${APP_NAME}:provider:${name}:token`, value);
}
})
);
Spicetify.PopupModal.display({
title: "Lyrics Plus",
content: configContainer,
isLarge: true
});
}