Tweaks to Emeraldchat: gender info, auto rejoin, auto-karma, and keeping chat input focused.
目前為
// ==UserScript==
// @name EmeraldX
// @namespace https://greasyfork.org/
// @version 1.9
// @description Tweaks to Emeraldchat: gender info, auto rejoin, auto-karma, and keeping chat input focused.
// @author Zach
// @license GPL-3.0
// @icon https://emeraldchat.com/logo7.svg
// @match https://emeraldchat.com/app
// @grant none
// @compatible Firefox
// @compatible Violentmonkey
// ==/UserScript==
'use strict';
let uiVisible = true;
let autoNextEnabled = false;
let lastMatchedId = null;
// === UI CREATION ===
function createUI() {
const topUI = document.createElement("div");
topUI.id = "top-ui";
topUI.innerHTML = `
<div id="profile-info" class="section">
<p id="user-gender">Gender: Not Available</p>
<p id="user-interests">Interests: None</p>
</div>
<style>
#top-ui {
width: 100%;
background: rgba(200,200,255,0.1);
border-bottom: 1px solid rgba(255,255,255,0.15);
backdrop-filter: blur(6px);
font-family: "Segoe UI", Roboto, sans-serif;
transition: .2s;
}
.hidden { height: 0; opacity: 0; padding: 0; margin: 0; }
.section { display: flex; flex-direction: column; padding: 6px 12px; gap: 4px; }
#user-gender, #user-interests {
background: rgba(255,255,255,0.2);
border-radius: 8px;
padding: 6px 10px;
font-size: 13px;
color: #fff;
}
#user-gender.male { background: rgba(0,120,255,0.25); }
#user-gender.female { background: rgba(255,105,180,0.25); }
</style>
`;
const insertUI = () => {
const messages = document.getElementById("messages");
if (messages && !document.getElementById("top-ui"))
messages.parentNode.insertBefore(topUI, messages);
applyTopUIVisibility();
};
new MutationObserver(insertUI).observe(document.body, { childList: true, subtree: true });
createSettingsButton();
}
// === SETTINGS MENU ===
function createSettingsButton() {
const nav = document.querySelector(".navigation-notification-icons");
if (!nav) return;
// === Add settings button ===
nav.insertAdjacentHTML(
"afterbegin",
`<span id="settings-toggle" class="material-icons navigation-notification-unit">tune</span>`
);
const toggleBtn = document.getElementById("settings-toggle");
const menu = document.createElement("div");
menu.id = "settings-menu";
menu.innerHTML = `
<button class="menu-btn menu-toggle" id="toggle-ui">
<span>Show Top UI</span>
<label class="switch"><input type="checkbox" id="ui-checkbox"><span class="slider"></span></label>
</button>
<button class="menu-btn menu-toggle" id="auto-next">
<span>Auto Rejoin</span>
<label class="switch"><input type="checkbox" id="auto-next-checkbox"><span class="slider"></span></label>
</button>
<div class="menu-btn" id="gender-filter">
<span>Gender Preference</span>
<div class="segmented">
<label><input type="radio" name="gender" value="all" checked><span>All</span></label>
<label><input type="radio" name="gender" value="men"><span>Men</span></label>
<label><input type="radio" name="gender" value="women"><span>Women</span></label>
</div>
</div>
<div class="menu-btn" id="blacklist-row">
<span>Blacklist</span>
<input id="blacklist-input" type="text" placeholder="Enter keywords...">
</div>
<style>
/* === Base menu === */
#settings-menu {
position: fixed;
display: none;
flex-direction: column;
gap: 10px;
background: rgba(200,200,255,0.12);
border: 1px solid rgba(255,255,255,0.18);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 10px;
min-width: 220px;
color: #fff;
font-family: "Segoe UI", Roboto, sans-serif;
font-size: 14px;
z-index: 1000;
box-sizing: border-box;
}
.menu-btn {
display: flex;
justify-content: space-between;
align-items: center;
border: none;
border-radius: 8px;
padding: 10px 12px;
background: rgba(255,255,255,0.1);
color: #fff;
cursor: pointer;
width: 100%;
outline: none;
-webkit-tap-highlight-color: transparent;
}
#blacklist-input {
flex: 1;
margin-left: 10px;
border: none;
outline: none;
border-radius: 8px;
padding: 6px 10px;
background: rgba(255,255,255,0.15);
color: #fff;
font-size: 13px;
height: 28px;
}
/* === Toggle switch === */
.switch {
position: relative;
width: 36px;
height: 18px;
flex-shrink: 0;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(255,255,255,0.35);
border-radius: 20px;
transition: .25s;
}
.slider:before {
content: "";
position: absolute;
width: 14px; height: 14px;
left: 2px; top: 2px;
background: white;
border-radius: 50%;
transition: .25s;
}
input:checked + .slider { background: rgba(0,150,255,0.5); }
input:checked + .slider:before { transform: translateX(18px); }
/* === Segmented control (Gender Preference) === */
.segmented {
display: flex;
width: 100%;
border-radius: 8px;
overflow: hidden;
background: rgba(255,255,255,0.08);
margin-left: 8px;
border: 1px solid rgba(255,255,255,0.15);
}
.segmented label {
flex: 1; /* equal width for all */
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
padding: 6px 0;
font-size: 13px;
border-right: 1px solid rgba(255,255,255,0.15);
transition: background .2s ease, font-weight .2s ease;
}
.segmented label:last-child {
border-right: none;
}
.segmented input {
display: none;
}
.segmented span {
white-space: nowrap;
pointer-events: none;
}
/* unified color and hover logic */
.segmented label:hover {
background: rgba(255,255,255,0.12);
}
.segmented label:has(input:checked) {
background: rgba(0,150,255,0.4);
font-weight: 500;
}
</style>
`;
document.body.appendChild(menu);
// === Checkbox and radio logic ===
const uiCheckbox = menu.querySelector("#ui-checkbox");
const autoNextCheckbox = menu.querySelector("#auto-next-checkbox");
const genderRadios = menu.querySelectorAll('input[name="gender"]');
// === Position dropdown ===
function positionMenu() {
const rect = toggleBtn.getBoundingClientRect();
const menuWidth = menu.offsetWidth || 240;
const pad = 10;
let left = rect.left + rect.width / 2 - menuWidth / 2;
let top = rect.bottom + 6;
left = Math.min(Math.max(pad, left), window.innerWidth - menuWidth - pad);
menu.style.left = left + "px";
menu.style.top = top + "px";
}
// === Toggle menu ===
document.addEventListener("click", e => {
if (e.target === toggleBtn) {
const show = menu.style.display !== "flex";
menu.style.display = show ? "flex" : "none";
if (show) {
positionMenu();
uiCheckbox.checked = uiVisible;
autoNextCheckbox.checked = autoNextEnabled;
}
} else if (!menu.contains(e.target)) {
menu.style.display = "none";
}
});
// === Auto position on resize/scroll ===
new ResizeObserver(() => {
if (menu.style.display === "flex") positionMenu();
}).observe(document.body);
window.addEventListener("scroll", () => {
if (menu.style.display === "flex") positionMenu();
}, true);
// === Toggle logic ===
menu.addEventListener("click", e => {
const btn = e.target.closest(".menu-toggle");
if (!btn) return;
const box = btn.querySelector("input");
box.checked = !box.checked;
if (btn.id === "toggle-ui") {
uiVisible = box.checked;
applyTopUIVisibility();
}
if (btn.id === "auto-next") autoNextEnabled = box.checked;
});
// === Gender preference change ===
genderRadios.forEach(r =>
r.addEventListener("change", () => {
genderFilter = r.value; // "all", "men", "women"
applyGenderFilter?.(genderFilter);
})
);
}
// === CHAT BEHAVIOR ===
function focusChat() {
const chatInput = document.getElementById("room-input");
if (!chatInput) return;
document.body.addEventListener(
"keydown",
(event) => {
if (
!document.querySelector("#ui-hatch > *, #ui-hatch-2 > *, #interests") &&
event.key !== "`"
) {
chatInput.focus();
}
},
{ once: true }
);
}
// === FETCH USER INFO ===
function simulateProfileRequest() {
const matchEl = document.querySelector(
'#room .room-component-center #messages .room-component-print #matched-message[data-matched-id]'
);
const userId = matchEl?.dataset.matchedId;
if (!userId || userId === lastMatchedId) return;
lastMatchedId = userId;
fetch(`https://emeraldchat.com/profile_json?id=${userId}`)
.then((res) => res.json())
.then(({ user }) => {
const genderEl = document.getElementById("user-gender");
const interestsEl = document.getElementById("user-interests");
const gender = user.gender === "m" ? "Male" : "Female";
genderEl.textContent = `Gender: ${gender}`;
genderEl.className = user.gender === "m" ? "male" : "female";
const interests = user.interests?.map((i) => i.name).join(", ") || "None";
interestsEl.textContent = `Interests: ${interests}`;
})
.catch((err) => console.error("Profile fetch failed:", err));
}
// === SIMULATED CLICK ===
function simulateClick(el) {
["mousedown", "mouseup", "click"].forEach((type) =>
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }))
);
}
// === AUTO NEXT CHAT ===
function nextChat() {
if (!autoNextEnabled) return;
const next = document.querySelector("div.ui-button-match-mega");
if (next) simulateClick(next);
}
document.addEventListener("keydown", (e) => {
if (e.key === "`") autoNextEnabled = !autoNextEnabled;
});
// === AUTO KARMA ===
function giveKarma() {
const [good, bad] = document.querySelectorAll("a.ui-button-match-mega");
if (!good || !bad) return;
const messages = document.querySelectorAll("#messages > .room-component-message-container").length;
if (messages > 5) simulateClick(good);
else if (messages < 2) simulateClick(bad);
}
// === OBSERVER ===
function observeChanges() {
const container = document.getElementById("container");
if (!container) return;
new MutationObserver(() => {
giveKarma();
nextChat();
simulateProfileRequest();
focusChat();
}).observe(container, { childList: true, subtree: true });
}
// === HELPERS ===
function applyTopUIVisibility() {
const ui = document.getElementById("top-ui");
if (ui) ui.classList.toggle("hidden", !uiVisible);
}
// === INIT ===
createUI();
observeChanges();