您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Moves the vote counts in neal.fun/internet-roadtrip from the top right panel to be alongside the arrows, on the wheel, and in the radio
当前为
// ==UserScript== // @name Internet Roadtrip - Combined Votes Counts UI // @description Moves the vote counts in neal.fun/internet-roadtrip from the top right panel to be alongside the arrows, on the wheel, and in the radio // @namespace me.netux.site/user-scripts/internet-roadtrip/combined-votes-counts-ui // @version 1.4.0 // @author netux // @license MIT // @match https://neal.fun/internet-roadtrip/* // @icon https://neal.fun/favicons/internet-roadtrip.png // @run-at document-start // @grant GM_addStyle // @grant GM.getValue // @grant GM.setValue // @require https://cdn.jsdelivr.net/npm/[email protected] // ==/UserScript== /* globals IRF */ (async () => { const CSS_PREFIX = `cvcui-`; const cssClass = (names) => (Array.isArray(names) ? names : [names]).map((name) => `${CSS_PREFIX}${name}`).join(' '); await IRF.dom.container; GM_addStyle(` .container { & .results { top: 50px; right: 10px; width: fit-content; min-width: 200px; padding: 7px 10px; &::after { /* annoying... */ pointer-events: none; } & .results-content { padding-bottom: 6px; display: none; } & .${cssClass('results-content-toggle-button')} { width: 100%; height: 0.6rem; margin-block: 0.3rem 0.1rem; background-image: url("https://www.svgrepo.com/show/257732/up-arrow.svg"); background-size: contain; background-position: center; background-repeat: no-repeat; cursor: pointer; display: block; } &.${cssClass('results-content-open')} { & .${cssClass('results-content-toggle-button')} { rotate: 180deg; } & .results-content { display: revert; } } } .${cssClass('vote-count')} { position: absolute; font-family: "Roboto", sans-serif; color: white; text-shadow: ${[[0, 1], [0, -1], [1, 0], [-1, 0]].map(([x, y]) => `${x}px ${y}px 2px black`).join(', ')}; pointer-events: none; white-space: nowrap; } & .options { cursor: pointer; & .${cssClass('vote-count')} { bottom: -0.4em; left: 0; width: 100%; text-align: center; font-size: 12px; } } & .wheel-container { & .${cssClass('vote-count')} { top: 22%; left: 50%; translate: -50%; font-size: 20px; user-select: none; } } } body:not(.${cssClass('reduce-arrow-motion')}) .container :is( .option .option-arrow, .option .${cssClass('vote-count')} ) { transition: translate 0.1s linear; translate: 0 calc(-20px * var(--${CSS_PREFIX}vote-count-percentage)); } @media (max-width: 900px) { .container { & .results { top: 41px; right: 5px; } } } `); const containerVDOM = await IRF.vdom.container; const resultsEl = await IRF.dom.results; const resultsVDOM = await IRF.vdom.results; const optionsContainerEl = await IRF.dom.options; const wheelContainerEl = await IRF.dom.wheel; const radioEl = await IRF.dom.radio; const mapSound = await IRF.vdom.map.then((map) => map.data.mapSound); // yoink const wheelHonkVotesEl = document.createElement('span'); const radioSeekVotesTextNode = document.createTextNode('0'); function ensureOptionVotesEl(optionEl) { let votesEl = optionEl._votesEl; if (!votesEl) { votesEl = document.createElement('span'); votesEl.className = cssClass('vote-count'); votesEl.textContent = `0 (0%)`; optionEl.appendChild(votesEl); optionEl._votesEl = votesEl; } return votesEl; } function updateVotes(votes) { const totalVotes = Object.values(votes).reduce((total, count) => total + count, 0); const optionEls = optionsContainerEl.querySelectorAll('.option'); for (const [voteStr, votesCount] of Object.entries(votes)) { const percentage = totalVotes !== 0 ? (votesCount / totalVotes) : 0; const percentageStr = `${Math.floor(percentage * 100)}`; switch (voteStr) { case "-2": { wheelHonkVotesEl.textContent = `${votesCount} (${percentageStr})`; break; } case "-1": { radioSeekVotesTextNode.textContent = votesCount; break; } default: { const voteIndex = parseInt(voteStr, 10); const optionEl = optionEls[voteIndex]; if (!optionEl) { continue; } const votesEl = ensureOptionVotesEl(optionEl); votesEl.textContent = `${votesCount} (${percentageStr})`; optionEl.style.setProperty(`--${CSS_PREFIX}vote-count-percentage`, percentage); } } } } { const { set: voteCountsSetter } = Object.getOwnPropertyDescriptor(resultsVDOM.state._props, 'voteCounts'); Object.defineProperty(resultsVDOM.state._props, 'voteCounts', { set(newVoteCounts) { updateVotes(newVoteCounts); return voteCountsSetter.call(this, newVoteCounts); }, configurable: true, enumerable: true, }); } const settings = { 'results-content-open': false, 'reduce-arrow-motion': false }; for (const key in settings) { const value = await GM.getValue(key, settings[key]); settings[key] = value; } async function updateDomFromSettings() { document.body.classList.toggle(cssClass('reduce-arrow-motion'), settings['reduce-arrow-motion']); resultsEl.classList.toggle(cssClass('results-content-open'), settings['results-content-open']); } updateDomFromSettings(); async function saveSettings() { for (const key in settings) { await GM.setValue(key, settings[key]); } } { const optionsContainerMutationObserver = new MutationObserver((records) => { for (const record of records) { if (record.type !== "childList") { continue; } for (const addedOptionEl of record.addedNodes) { if (!addedOptionEl.classList?.contains('option')) { continue; } ensureOptionVotesEl(addedOptionEl); } } }); optionsContainerMutationObserver.observe(optionsContainerEl, { childList: true }); const wheelClickArealEl = wheelContainerEl.querySelector('.wheel-click-area'); wheelHonkVotesEl.className = cssClass('vote-count'); wheelClickArealEl.appendChild(wheelHonkVotesEl); const radioSeekButtonLabelEl = radioEl.querySelector('.control-button .button-label'); radioSeekButtonLabelEl.append( document.createTextNode(' ('), radioSeekVotesTextNode, document.createTextNode(')'), ); const resultsContentToggleEl = document.createElement('div'); resultsContentToggleEl.className = cssClass('results-content-toggle-button'); resultsContentToggleEl.addEventListener('click', async () => { mapSound?.play(); settings['results-content-open'] = !settings['results-content-open']; await saveSettings(); updateDomFromSettings(); }); const resultsContentEl = resultsEl.querySelector('.results-content'); resultsContentEl.insertAdjacentElement('afterend', resultsContentToggleEl); } { const tab = IRF.ui.panel.createTabFor( { ... GM.info, script: { ... GM.info.script, name: GM.info.script.name.replace('Internet Roadtrip - ', '') } }, { tabName: 'Combine Votes Counts UI', style: ` .${cssClass('settings-tab-content')} { & *, *::before, *::after { box-sizing: border-box; } & .${cssClass('field-group')} { display: flex; align-items: center; justify-content: space-between; } } `, className: cssClass('settings-tab-content'), } ); function makeFieldGroup({ id, label }, renderInput) { const fieldGroupEl = document.createElement('div'); fieldGroupEl.className = cssClass('field-group'); const labelEl = document.createElement('label'); labelEl.textContent = label; fieldGroupEl.appendChild(labelEl); const inputEl = renderInput({ id }); fieldGroupEl.appendChild(inputEl); return fieldGroupEl; } tab.container.append( makeFieldGroup({ id: `${CSS_PREFIX}reduce-arrow-motion`, label: 'Reduce Arrow Motion' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.id = id; inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = settings['reduce-arrow-motion']; inputEl.addEventListener('change', async () => { settings['reduce-arrow-motion'] = inputEl.checked; await saveSettings(); updateDomFromSettings(); }); return inputEl; }) ); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址