您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show the result of the last votes in neal.fun/internet-roadtrip
// ==UserScript== // @name Internet Roadtrip - Vote History // @description Show the result of the last votes in neal.fun/internet-roadtrip // @namespace me.netux.site/user-scripts/internet-roadtrip/vote-history // @version 2.1.1 // @author Netux // @license MIT // @match https://neal.fun/internet-roadtrip/* // @icon https://cloudy.netux.site/neal_internet_roadtrip/Vote%20History%20logo.png // @grant GM_addStyle // @grant GM.getValue // @grant GM.setValue // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected] // @noframes // ==/UserScript== (async () => { const CSS_PREFIX = 'vh-'; const cssClass = (... names) => names.map((name) => `${CSS_PREFIX}${name}`).join(' '); const cssProp = (name) => `--${CSS_PREFIX}${name}`; const state = { dom: {}, settings: { increasedVisibility: false, maxEntries: 8, debug: { honkers: false } } }; for (const [key, defaultValue] of Object.entries(state.settings)) { state.settings[key] = await GM.getValue(key, defaultValue); } function injectStylesheets() { GM_addStyle(` .${cssClass('vote-history')} { position: fixed; left: 10px; top: 150px; margin: 0; padding: 0; list-style: none; color: white; font-family: "Roboto", sans-serif; font-size: 0.8rem; user-select: none; & .${cssClass('vote-history-entry')} { margin: 0.5rem 0; text-shadow: 1px 1px 2px black; display: flex; align-items: center; .${cssClass('vote-history--increased-visibility')} & { text-shadow: ${[[0, 1], [0, -1], [1, 0], [-1, 0]].map(([x, y]) => `${x}px ${y}px 2px rgba(0 0 0 / 50%)`)}; } & .${cssClass('vote-history-entry__icon-container')} { position: relative; height: 12px; aspect-ratio: 1; margin-right: 0.5rem; vertical-align: middle; & > img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: inline-block; } & .${cssClass('vote-history-entry__icon-shadow')} { opacity: .5; scale: 1.5; filter: blur(5px); } & .${cssClass('vote-history-entry__icon')} { filter: invert(1); z-index: 1; } } & .${cssClass('vote-history-entry__time')} { margin-left: 1ch; font-size: 80%; color: lightgrey; } } } `); state.dom.voteHistoryEntriesFadeStyleEl = document.createElement('style'); document.head.appendChild(state.dom.voteHistoryEntriesFadeStyleEl); } function createSettingsTab() { const tab = IRF.ui.panel.createTabFor( { ... GM.info, script: { ... GM.info.script, name: GM.info.script.name.replace('Internet Roadtrip - ', '') } }, { tabName: 'Vote History', style: ` .${cssClass('settings-tab-content')} { & *, *::before, *::after { box-sizing: border-box; } & .${cssClass('field-group')} { margin-block: 1rem; gap: 0.25rem; display: flex; align-items: center; justify-content: space-between; & input:is(:not([type]), [type="text"], [type="number"]) { --padding-inline: 0.5rem; width: calc(100% - 2 * var(--padding-inline)); min-height: 1.5rem; margin: 0; padding-inline: var(--padding-inline); color: white; background: transparent; border: 1px solid #848e95; font-size: 100%; border-radius: 5rem; } } } `, 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}max-entries`, label: 'Past Votes Max Entries' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.id = id; inputEl.type = 'number'; inputEl.style.width = '10ch'; inputEl.value = state.settings.maxEntries; inputEl.addEventListener('change', async () => { const numberValue = parseInt(inputEl.value); if (Number.isNaN(numberValue)) { return; } state.settings.maxEntries = numberValue; await saveSettings(); updateDomFromSettings(); }); return inputEl; }), makeFieldGroup({ id: `${CSS_PREFIX}increase-visiblity`, label: 'Increase Visibility' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.id = id; inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = state.settings.increasedVisibility; inputEl.addEventListener('change', async () => { state.settings.increasedVisibility = inputEl.checked; await saveSettings(); updateDomFromSettings(); }); return inputEl; }), ); } async function setupDom() { await IRF.vdom.container; // FIX(netux): required to avoid crashing other userscripts :s const containerEl = await IRF.dom.container; injectStylesheets(); createSettingsTab(); state.dom.voteHistoryContainerEl = document.createElement('ul'); state.dom.voteHistoryContainerEl.className = cssClass('vote-history'); containerEl.appendChild(state.dom.voteHistoryContainerEl); updateDomFromSettings(); } async function patch() { const containerVDOM = await IRF.vdom.container; containerVDOM.state.changeStop = new Proxy(containerVDOM.methods.changeStop, { apply(ogChangeStop, thisArg, args) { const currentChosen = args[1]; if (!thisArg.isChangingStop) { const payload = { currentChosen, currentHeading: thisArg.currentHeading, currentOptions: thisArg.currentOptions, voteCounts: thisArg.voteCounts }; addVote(payload).catch((error) => { console.error('Could not add vote to history:', payload, error); }) } return ogChangeStop.apply(thisArg, args); } }); } async function saveSettings() { for (const [key, value] of Object.entries(state.settings)) { await GM.setValue(key, value); } } function updateDomFromSettings() { removeExcessVotes(); // TODO(netux): it'd be nice to be able to use CSS variables and counters here, // but counter() does not work inside calc() :( // See https://github.com/w3c/csswg-drafts/issues/1026 state.dom.voteHistoryEntriesFadeStyleEl.textContent = ` .${cssClass('vote-history')} { ${new Array(state.settings.maxEntries).fill(null).map((_, i) => ` .${cssClass('vote-history-entry')}:nth-child(${i + 1}) { opacity: ${1.2 - Math.pow(1 - ((state.settings.maxEntries - i) / state.settings.maxEntries), 2)}; } `).join('\n')} } `; state.dom.voteHistoryContainerEl.classList.toggle(cssClass('vote-history--increased-visibility'), state.settings.increasedVisibility); } function removeExcessVotes() { while (state.dom.voteHistoryContainerEl.childElementCount > Math.max(0, state.settings.maxEntries)) { state.dom.voteHistoryContainerEl.lastElementChild.remove(); } } async function addVote({ currentChosen: vote, currentHeading: heading, currentOptions: options, voteCounts }) { const resultsVDOM = await IRF.vdom.results; const newEntryEl = document.createElement('li'); newEntryEl.className = cssClass('vote-history-entry'); let entryActionText = '?'; let entryIconSrc = null; let entryIconRotation = 0; switch (vote) { case -2: { entryActionText = 'HONK!'; entryIconSrc = '/internet-roadtrip/honk.svg' break; } case -1: { entryActionText = 'Seek Radio'; entryIconSrc = '/internet-roadtrip/skip.svg' break; } default: { entryIconSrc = '/internet-roadtrip/chevron-black.svg'; if (options[vote]) { entryActionText = options[vote].description; entryIconRotation = resultsVDOM.methods.getRotation(vote); } else { entryActionText = 'U-Turn'; entryIconRotation = 180; } break; } } const voteIconEl = document.createElement('div'); voteIconEl.className = cssClass('vote-history-entry__icon-container'); if (entryIconRotation !== 0) { voteIconEl.style.rotate = `${entryIconRotation}deg`; } newEntryEl.appendChild(voteIconEl); const voteIconShadowImageEl = document.createElement('img'); voteIconShadowImageEl.className = cssClass('vote-history-entry__icon-shadow'); voteIconShadowImageEl.src = entryIconSrc; voteIconEl.appendChild(voteIconShadowImageEl); const voteIconImageEl = document.createElement('img'); voteIconImageEl.className = cssClass('vote-history-entry__icon'); voteIconImageEl.src = entryIconSrc; voteIconEl.appendChild(voteIconImageEl); const voteCount = voteCounts[vote]; const entryVotesText = voteCount != null ? `${voteCount} vote${voteCount === 1 ? '' : 's'}, ${Math.round(voteCount / Object.values(voteCounts).reduce((acc, votes) => acc + votes, 0) * 100)}%` : 'no votes'; const entryTextNode = document.createTextNode(`${entryActionText} (${entryVotesText})`); newEntryEl.appendChild(entryTextNode); const entryTimeEl = document.createElement('span'); entryTimeEl.className = cssClass('vote-history-entry__time'); entryTimeEl.innerText = new Date().toLocaleTimeString(); newEntryEl.appendChild(entryTimeEl); if (state.dom.voteHistoryContainerEl.childElementCount > 0) { state.dom.voteHistoryContainerEl.insertBefore(newEntryEl, state.dom.voteHistoryContainerEl.firstChild); } else { state.dom.voteHistoryContainerEl.appendChild(newEntryEl); } removeExcessVotes(); } if (typeof unsafeWindow !== 'undefined') { unsafeWindow.DEBUG__addVoteToHistory = addVote; } await Promise.all([ setupDom(), patch() ]) .then(async () => { if (state.settings.debug?.honkers) { for (let i = 0; i < state.settings.maxEntries; i++) { const vote = -2; await addVote({ currentChosen: vote, currentHeading: 0, currentOptions: { [vote]: { heading: 180 } }, voteCounts: { [vote]: 100, [9999]: 50 } }); } } }); })();