您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Desktop-only fork of Return YouTube Dislike. Adds Shorts SPA support, visual like/dislike ratio bar, and a class-based architecture.
// ==UserScript== // @name Return YouTube Dislike (PC Only) // @name:ja Return YouTube Dislike(PC専用改良版) // @namespace https://github.com/koyasi777/return-youtube-dislike-pc-only // @homepage https://github.com/koyasi777/return-youtube-dislike-pc-only // @version 4.1.0 // @encoding utf-8 // @description Desktop-only fork of Return YouTube Dislike. Adds Shorts SPA support, visual like/dislike ratio bar, and a class-based architecture. // @description:ja デスクトップ専用に改良した Return YouTube Dislike。Shorts対応、評価バー表示、クラスベース設計を追加。 // @icon https://github.com/Anarios/return-youtube-dislike/raw/main/Icons/Return%20Youtube%20Dislike%20-%20Transparent.png // @author Anarios & JRWR (original), koyasi777 (mod) // @license AGPL-3.0-only // @licenseURL https://www.gnu.org/licenses/agpl-3.0.html // @match *://*.youtube.com/* // @exclude *://music.youtube.com/* // @exclude *://*.music.youtube.com/* // @compatible chrome // @compatible firefox // @compatible opera // @compatible safari // @compatible edge // @grant GM.xmlHttpRequest // @grant GM_addStyle // @connect youtube.com // @run-at document-end // ==/UserScript== (function () { 'use strict'; class ReturnYouTubeDislike { // --- Static Properties --- static config = { disableLogging: true, coloredThumbs: false, coloredBar: false, colorTheme: "classic", numberDisplayFormat: "compactShort", numberDisplayRoundDown: true, tooltipPercentageMode: "none", numberDisplayReformatLikes: false, rateBarEnabled: true }; static API_URL = 'https://returnyoutubedislikeapi.com/votes?videoId='; static VOTE_STATE = { LIKED: 1, DISLIKED: 2, NEUTRAL: 3 }; static STYLES = ` #return-youtube-dislike-bar-container { background: var(--yt-spec-icon-disabled); border-radius: 2px; } #return-youtube-dislike-bar { background: var(--yt-spec-text-primary); border-radius: 2px; transition: all 0.15s ease-in-out; } .ryd-tooltip { position: absolute; display: block; height: 1.5px; bottom: -10px; } #ryd-dislike-tooltip { visibility: hidden; opacity: 0; transition: opacity 0.15s ease-in-out; background-color: #616161; color: #fff; padding: 8px; border-radius: 2px; font-size: 12px; text-align: center; white-space: nowrap; } .ryd-tooltip:hover #ryd-dislike-tooltip { visibility: visible; opacity: 1; } .ryd-tooltip-bar-container { width: 100%; height: 2px; position: absolute; padding-top: 6px; padding-bottom: 12px; top: -6px; } ytd-menu-renderer.ytd-watch-metadata { overflow-y: visible !important; } #top-level-buttons-computed { position: relative !important; } #ryd-shorts-dislike-text { color: var(--yt-spec-text-primary); font-family: "Roboto", "Arial", sans-serif; font-size: 12px; font-weight: 500; text-align: center; margin-top: 4px; } `; // --- Instance Properties --- videoId = null; likes = 0; dislikes = 0; previousState = ReturnYouTubeDislike.VOTE_STATE.NEUTRAL; domCache = {}; initInterval = null; rateBarObserver = null; debouncedUpdateRateBar = null; constructor() { this.log('Instance created.'); this.injectStyles(); this.debouncedUpdateRateBar = this.debounce(() => this.createOrUpdateRateBar(), 150); window.addEventListener('yt-navigate-finish', () => this.run(), true); this.run(); } run() { if (this.initInterval) clearInterval(this.initInterval); this.initInterval = setInterval(() => this.checkForDOMReady(), 100); } checkForDOMReady() { const currentVideoId = this.getVideoId(); if (!currentVideoId || (currentVideoId === this.videoId && this.domCache.buttons)) return; if (!currentVideoId && this.videoId) { this.cleanup(); return; } const buttons = this.getButtons(); if (buttons?.offsetParent && (this.isShorts() || this.isRegularVideoLoaded())) { this.log('DOM is ready. Initializing main logic.'); clearInterval(this.initInterval); this.initInterval = null; this.cleanup(); this.main(currentVideoId, buttons); } } async main(videoId, buttons) { this.videoId = videoId; this.log(`Processing video ID: ${this.videoId}`); if (!this.cacheDomElements(buttons)) { this.logError('Required DOM elements not found. Aborting.'); return; } try { const data = await this.fetchVotes(); this.dislikes = data.dislikes; this.likes = this.getNativeLikeCount() ?? data.likes; this.log(`Using like count: ${this.likes}`); this.previousState = this.getInitialVoteState(); this.updateUI(); this.setupEventListeners(); this.setupRateBarObserver(); } catch (error) { this.logError('Failed to fetch or process votes.', error); } } cleanup() { this.log('Cleaning up previous instance.'); if (this.initInterval) clearInterval(this.initInterval); if (this.rateBarObserver) this.rateBarObserver.disconnect(); this.initInterval = null; this.rateBarObserver = null; this.domCache = {}; this.videoId = null; } setupRateBarObserver() { if (!ReturnYouTubeDislike.config.rateBarEnabled || this.isShorts() || !this.domCache.buttons) return; this.rateBarObserver = new MutationObserver(() => { this.log('Button container changed. Debouncing rate bar update.'); this.debouncedUpdateRateBar(); }); this.rateBarObserver.observe(this.domCache.buttons, { childList: true, subtree: true, attributes: true }); } /** * 🚀 [修正] 永続オブザーバーと自己再試行ポーリングを組み合わせたハイブリッド方式 */ createOrUpdateRateBar(retryCount = 0) { const { buttons, likeButton, dislikeButton } = this.domCache; if (!buttons || this.isShorts() || !likeButton || !dislikeButton) return; const likeButtonWidth = likeButton.clientWidth; const dislikeButtonWidth = dislikeButton.clientWidth; const totalWidth = likeButtonWidth + dislikeButtonWidth; // 起動時の描画タイミング問題に対応するため、自己再試行ロジックを復活させる if (totalWidth === 0) { const maxRetries = 10; if (retryCount < maxRetries) { this.log(`Rate bar calculation skipped: button width is zero. Retrying... (${retryCount + 1}/${maxRetries})`); setTimeout(() => this.createOrUpdateRateBar(retryCount + 1), 250); } else { this.logError('Failed to get button width for rate bar after multiple retries.'); } return; } let rateBarContainer = document.getElementById('return-youtube-dislike-bar-container'); const totalVotes = this.likes + this.dislikes; const likePercentage = totalVotes > 0 ? (this.likes / totalVotes) * 100 : 50; if (!rateBarContainer) { this.log(`Creating rate bar. Width: ${totalWidth}px`); const tooltipHtml = this.getTooltipHtml(likePercentage); const colorLikeStyle = ReturnYouTubeDislike.config.coloredBar ? `background-color: ${this.getColor(true)};` : ''; const colorDislikeStyle = ReturnYouTubeDislike.config.coloredBar ? `background-color: ${this.getColor(false)};` : ''; const barHtml = `<div class="ryd-tooltip" style="width: ${totalWidth}px"><div class="ryd-tooltip-bar-container"><div id="return-youtube-dislike-bar-container" style="width: 100%; height: 2px; ${colorDislikeStyle}"><div id="return-youtube-dislike-bar" style="width: ${likePercentage}%; height: 100%; ${colorLikeStyle}"></div></div></div><tp-yt-paper-tooltip position="top" id="ryd-dislike-tooltip" role="tooltip" tabindex="-1">${tooltipHtml}</tp-yt-paper-tooltip></div>`; buttons.insertAdjacentHTML('beforeend', barHtml); } else { this.log(`Updating rate bar. Width: ${totalWidth}px`); document.querySelector('.ryd-tooltip').style.width = `${totalWidth}px`; document.getElementById('return-youtube-dislike-bar').style.width = `${likePercentage}%`; document.getElementById('ryd-dislike-tooltip').innerHTML = this.getTooltipHtml(likePercentage); } } // --- Unchanged Helper Methods --- cacheDomElements(buttonsContainer){this.log("Caching DOM elements..."),this.domCache.buttons=buttonsContainer;const e=this.isShorts()?"#like-button":"ytd-segmented-like-dislike-button-renderer #like-button, ytd-toggle-button-renderer:first-of-type, like-button-view-model",t=this.isShorts()?"#dislike-button":"ytd-segmented-like-dislike-button-renderer #dislike-button, ytd-toggle-button-renderer:nth-of-type(2), dislike-button-view-model";if(this.domCache.likeButton=buttonsContainer.querySelector(e),this.domCache.dislikeButton=buttonsContainer.querySelector(t),!this.domCache.likeButton||!this.domCache.dislikeButton)return!1;if(this.isShorts()){let o=document.getElementById("ryd-shorts-dislike-text");o&&!o.isConnected&&(o=null),o||(o=document.createElement("span"),o.id="ryd-shorts-dislike-text",this.domCache.dislikeButton.insertAdjacentElement("afterend",o)),this.domCache.dislikeText=o}else{if(this.domCache.likeText=this.domCache.likeButton.querySelector("#text, .yt-spec-button-shape-next__button-text-content"),this.domCache.dislikeText=this.domCache.dislikeButton.querySelector("#text, .yt-spec-button-shape-next__button-text-content"),!this.domCache.dislikeText){let i=this.domCache.dislikeButton.querySelector("button")||this.domCache.dislikeButton;if(i.querySelector("#text"))return!0;let s=document.createElement("span");s.id="text",s.className="yt-spec-button-shape-next__button-text-content",s.style.marginLeft="6px",i.style.width="auto",i.appendChild(s),this.domCache.dislikeText=s}}return!0} isRegularVideoLoaded(){return document.querySelector(`ytd-watch-grid[video-id='${this.getVideoId()}']`)||document.querySelector(`ytd-watch-flexy[video-id='${this.getVideoId()}']`)} getButtons(){if(this.isShorts()){for(const e of document.querySelectorAll("#actions.ytd-reel-player-overlay-renderer")){const t=e.getBoundingClientRect();if(t.top>0&&t.bottom<window.innerHeight&&t.width>0&&t.height>0)return e}return null}return(document.getElementById("menu-container")?.offsetParent===null?document.querySelector("ytd-menu-renderer.ytd-watch-metadata > div")||document.querySelector("ytd-menu-renderer.ytd-video-primary-info-renderer > div"):document.getElementById("menu-container")?.querySelector("#top-level-buttons-computed"))} fetchVotes(){return new Promise((e,t)=>{GM.xmlHttpRequest({method:"GET",url:`${ReturnYouTubeDislike.API_URL}${this.videoId}`,onload:o=>{if(o.status>=200&&o.status<300)try{const n=JSON.parse(o.responseText);n&&typeof n.dislikes!="undefined"?(this.log(`Received counts: ${n.dislikes} dislikes, ${n.likes} likes.`),e(n)):t(new Error("Invalid API response format."))}catch(n){t(new Error("Failed to parse API response."))}else t(new Error(`API request failed with status: ${o.status}`))},onerror:e=>t(e),ontimeout:()=>t(new Error("API request timed out.")),timeout:15e3})})} getInitialVoteState(){const{likeButton:e,dislikeButton:t}=this.domCache;return e?.getAttribute("aria-pressed")==="true"||e?.classList.contains("style-default-active")?ReturnYouTubeDislike.VOTE_STATE.LIKED:t?.getAttribute("aria-pressed")==="true"||t?.classList.contains("style-default-active")?ReturnYouTubeDislike.VOTE_STATE.DISLIKED:ReturnYouTubeDislike.VOTE_STATE.NEUTRAL} getNativeLikeCount(){if(!this.domCache.likeButton)return null;try{const e=this.domCache.likeButton.querySelector("button")||this.domCache.likeButton,t=(e.getAttribute("aria-label")||"").replace(/\D/g,"");return t?parseInt(t,10):null}catch(e){return this.logError("Could not parse native like count from aria-label.",e),null}} updateUI(){this.updateDislikeCount(),ReturnYouTubeDislike.config.numberDisplayReformatLikes&&this.updateLikeCount(),ReturnYouTubeDislike.config.rateBarEnabled&&this.createOrUpdateRateBar(),ReturnYouTubeDislike.config.coloredThumbs&&this.applyThumbColors()} updateDislikeCount(){this.domCache.dislikeText&&(this.domCache.dislikeText.textContent=this.formatNumber(this.dislikes))} updateLikeCount(){this.domCache.likeText&&(this.domCache.likeText.textContent=this.formatNumber(this.likes))} applyThumbColors(){} setupEventListeners(){this.log("Registering button listeners..."),this.domCache.likeButton?.addEventListener("click",this.handleVoteClick.bind(this)),this.domCache.dislikeButton?.addEventListener("click",this.handleVoteClick.bind(this))} handleVoteClick(){if(this.isUserLoggedIn())setTimeout(()=>{const e=this.getInitialVoteState();if(e!==this.previousState)e===ReturnYouTubeDislike.VOTE_STATE.LIKED?(this.likes++,this.previousState===ReturnYouTubeDislike.VOTE_STATE.DISLIKED&&this.dislikes--):e===ReturnYouTubeDislike.VOTE_STATE.DISLIKED?(this.dislikes++,this.previousState===ReturnYouTubeDislike.VOTE_STATE.LIKED&&this.likes--):e===ReturnYouTubeDislike.VOTE_STATE.NEUTRAL&&(this.previousState===ReturnYouTubeDislike.VOTE_STATE.LIKED&&this.likes--,this.previousState===ReturnYouTubeDislike.VOTE_STATE.DISLIKED&&this.dislikes--),this.previousState=e,this.updateUI()},200)} debounce(e,t){let o=null;return(...n)=>{window.clearTimeout(o),o=window.setTimeout(()=>{e.apply(null,n)},t)}} log(e,t=""){ReturnYouTubeDislike.config.disableLogging||console.log(`[RYD] ${e}`,t)} logError(e,t){console.error(`[RYD] ERROR: ${e}`,t||"")} injectStyles(){GM_addStyle(ReturnYouTubeDislike.STYLES)} getVideoId(){const e=new URL(window.location.href);return e.pathname.startsWith("/shorts/")?e.pathname.split("/")[2]:e.pathname.startsWith("/clip/")?document.querySelector("meta[itemprop='videoId']")?.content||null:e.searchParams.get("v")} isShorts(){return window.location.pathname.startsWith("/shorts/")} isUserLoggedIn(){return document.querySelector("#avatar-btn")!==null} formatNumber(e){const t=ReturnYouTubeDislike.config.numberDisplayRoundDown?this.roundDown(e):e;return this.getNumberFormatter().format(t)} roundDown(e){if(e<1e3)return e;const t=Math.floor(Math.log10(e)-2),o=t+(t%3?1:0);return Math.floor(e/10**o)*10**o} getNumberFormatter(){const e=document.documentElement.lang||navigator.language||"en",t={compactShort:{notation:"compact",compactDisplay:"short"},compactLong:{notation:"compact",compactDisplay:"long"},standard:{notation:"standard"}};return new Intl.NumberFormat(e,t[ReturnYouTubeDislike.config.numberDisplayFormat]||t.compactShort)} getTooltipHtml(e){const t=100-e,o=this.likes.toLocaleString(),n=this.dislikes.toLocaleString();switch(ReturnYouTubeDislike.config.tooltipPercentageMode){case"dash_like":return`${o} / ${n} - ${e.toFixed(1)}%`;case"dash_dislike":return`${o} / ${n} - ${t.toFixed(1)}%`;case"both":return`${e.toFixed(1)}% / ${t.toFixed(1)}%`;case"only_like":return`${e.toFixed(1)}%`;case"only_dislike":return`${t.toFixed(1)}%`;default:return`${o} / ${n}`}} getColor(e){const t={classic:{like:"#00FF00",dislike:"#FF0000"},accessible:{like:"#2186F2",dislike:"#F8C100"},neon:{like:"#00FFFF",dislike:"#FF00FF"}},o=t[ReturnYouTubeDislike.config.colorTheme]||t.classic;return e?o.like:o.dislike} } new ReturnYouTubeDislike(); })();