您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为YouTube评论区添加缩放控件!滚轮缩放,点击重置,缩放等级会被保存。
// ==UserScript== // @name YouTubeコメント欄を拡大・縮小 🎥 // @name:ja YouTubeコメント欄を拡大・縮小 🎥 // @name:en Zoom Controls for YouTube Comments 🎥 // @name:zh-CN YouTube评论区缩放控制 🎥 // @name:zh-TW YouTube留言區縮放控制 🎥 // @name:ko YouTube 댓글 확대/축소 제어 🎥 // @name:fr Contrôle du zoom pour les commentaires YouTube 🎥 // @name:es Control de zoom en comentarios de YouTube 🎥 // @name:de Zoom-Steuerung für YouTube-Kommentare 🎥 // @name:pt-BR Controle de zoom nos comentários do YouTube 🎥 // @name:ru Управление масштабом комментариев на YouTube 🎥 // @version 2.1.1 // @description YouTubeのコメント欄を拡大・縮小するUIを追加!ホイールでズーム、クリックでリセット。状態は保存されます。 // @description:ja YouTubeのコメント欄を拡大・縮小するUIを追加!ホイールでズーム、クリックでリセット。状態は保存されます。 // @description:en Adds zoom controls to YouTube comments! Scroll to zoom in/out, click to reset. Zoom level is saved. // @description:zh-CN 为YouTube评论区添加缩放控件!滚轮缩放,点击重置,缩放等级会被保存。 // @description:zh-TW 為YouTube留言區新增縮放控制!滾輪縮放,點擊重設,縮放設定會被儲存。 // @description:ko YouTube 댓글에 확대/축소 UI 추가! 스크롤로 조절, 클릭으로 리셋. 설정은 저장됩니다. // @description:fr Ajoute un contrôle de zoom aux commentaires YouTube ! Molette pour zoomer, clic pour réinitialiser. Niveau de zoom sauvegardé. // @description:es Agrega controles de zoom a los comentarios de YouTube. Rueda para ampliar/reducir, clic para restablecer. El nivel se guarda. // @description:de Fügt Zoomsteuerung für YouTube-Kommentare hinzu! Scrollen zum Zoomen, Klick zum Zurücksetzen. Zoom-Level wird gespeichert. // @description:pt-BR Adiciona controles de zoom aos comentários do YouTube! Role para ajustar, clique para redefinir. Nível salvo. // @description:ru Добавляет управление масштабом к комментариям на YouTube. Прокрутка — зум, клик — сброс. Уровень сохраняется. // @namespace https://github.com/koyasi777/youtube-comment-zoom-control // @author koyasi777 // @match *://www.youtube.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @license MIT // @compatible firefox // @homepageURL https://github.com/koyasi777/youtube-comment-zoom-control // @supportURL https://github.com/koyasi777/youtube-comment-zoom-control/issues // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // ==/UserScript== (function() { 'use strict'; class CommentZoomManager { // --- 1. 設定 (Configuration) --- constructor() { this.pageType = this.getPageType(); this.containerSelector = ''; this.injectionTargetSelector = ''; this.zoomTargetSelector = ''; this.SCRIPT_ID = 'youtube-comment-zoom-controls-container'; this.STYLE_ID = 'youtube-comment-zoom-style-sheet'; this.ZOOM_STORAGE_KEY = 'yt-comment-zoom-level'; this.DEFAULT_ZOOM = 100; this.MIN_ZOOM = 50; this.MAX_ZOOM = 200; this.ZOOM_STEP = 5; // ツールチップの文言を定義 this.tooltips = { ja: 'スクロールで拡大縮小 / クリックでリセット', en: 'Scroll to zoom / Click to reset' }; this.setSelectors(); this.currentZoom = this.DEFAULT_ZOOM; this.uiObserver = null; } // --- 2. 初期化と破棄 (Initialization & Destruction) --- async init() { if (!this.pageType) { console.log('[CommentZoom] Not a watch or shorts page. Skipping initialization.'); return; } await this.loadZoomState(); this.injectStyles(); this.observeHeader(); console.log(`[CommentZoom] Initialized for ${this.pageType} page with zoom: ${this.currentZoom}%`); } stop() { if (this.uiObserver) { this.uiObserver.disconnect(); this.uiObserver = null; console.log('[CommentZoom] Observer stopped.'); } } getPageType() { if (location.pathname.startsWith('/watch')) return 'watch'; if (location.pathname.startsWith('/shorts')) return 'shorts'; return null; } setSelectors() { switch (this.pageType) { case 'watch': this.containerSelector = 'ytd-comments#comments'; this.injectionTargetSelector = 'ytd-comments-header-renderer #sort-menu'; this.zoomTargetSelector = 'ytd-comment-thread-renderer'; break; case 'shorts': this.containerSelector = 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"]'; this.injectionTargetSelector = 'ytd-engagement-panel-title-header-renderer #menu'; this.zoomTargetSelector = 'ytd-comment-thread-renderer'; break; } } // --- 3. 状態管理 (State Management) --- async loadZoomState() { let storedZoom = await GM_getValue(this.ZOOM_STORAGE_KEY, this.DEFAULT_ZOOM); if (typeof storedZoom !== 'number' || isNaN(storedZoom)) { storedZoom = this.DEFAULT_ZOOM; } this.currentZoom = storedZoom; } async saveZoomState() { await GM_setValue(this.ZOOM_STORAGE_KEY, this.currentZoom); } // --- 4. スタイル管理 (Style Management) --- injectStyles() { if (document.getElementById(this.STYLE_ID)) { this.updateZoomStyle(); return; } const styleElement = document.createElement('style'); styleElement.id = this.STYLE_ID; (document.head || document.documentElement).appendChild(styleElement); this.updateZoomStyle(); } updateZoomStyle() { const styleElement = document.getElementById(this.STYLE_ID); if (!styleElement) return; const staticStyles = ` ytd-comments-header-renderer #sort-menu #icon-label, ytd-engagement-panel-title-header-renderer #menu #label { font-size: 10.5px !important; } ytd-comments-header-renderer h2#count yt-formatted-string.count-text { font-size: 1.48rem !important; } `; const dynamicZoomStyle = this.currentZoom === 100 ? '' : `${this.zoomTargetSelector} { zoom: ${this.currentZoom}%; }`; styleElement.textContent = staticStyles + dynamicZoomStyle; } // --- 5. UIの生成とイベントハンドリング (UI Creation & Event Handling) --- createZoomControls() { const container = document.createElement('div'); container.id = this.SCRIPT_ID; Object.assign(container.style, { position: 'relative', // ツールチップの基準点として必要 display: 'flex', alignItems: 'center', marginLeft: '8px', padding: '4px 8px', borderRadius: '8px', transition: 'background-color 0.2s', cursor: 'pointer', userSelect: 'none' }); // ツールチップをYT標準コンポーネントで作成 const tooltip = document.createElement('tp-yt-paper-tooltip'); tooltip.setAttribute('role', 'tooltip'); const tooltipText = document.createElement('div'); tooltipText.id = 'tooltip'; tooltipText.className = 'style-scope tp-yt-paper-tooltip'; const lang = document.documentElement.lang.startsWith('ja') ? 'ja' : 'en'; tooltipText.textContent = this.tooltips[lang]; tooltip.appendChild(tooltipText); const zoomIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); zoomIcon.setAttribute('viewBox', '0 0 24 24'); Object.assign(zoomIcon.style, { width: '18px', height: '18px', marginRight: '6px', fill: 'var(--yt-spec-text-secondary)', transition: 'fill 0.2s', }); zoomIcon.innerHTML = `<path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path>`; const zoomDisplay = document.createElement('div'); Object.assign(zoomDisplay.style, { color: 'var(--yt-spec-text-secondary)', fontSize: '14px', fontWeight: '500', fontVariantNumeric: 'tabular-nums', transition: 'color 0.2s', }); const updateDisplay = () => { zoomDisplay.textContent = `${this.currentZoom}%`; }; updateDisplay(); const handleZoomUpdate = () => { this.saveZoomState(); this.updateZoomStyle(); updateDisplay(); }; container.onmouseenter = () => { container.style.backgroundColor = 'var(--yt-spec-badge-chip-background-hover)'; zoomIcon.style.fill = 'var(--yt-spec-text-primary)'; zoomDisplay.style.color = 'var(--yt-spec-text-primary)'; }; container.onmouseleave = () => { container.style.backgroundColor = 'transparent'; zoomIcon.style.fill = 'var(--yt-spec-text-secondary)'; zoomDisplay.style.color = 'var(--yt-spec-text-secondary)'; }; container.addEventListener('wheel', (e) => { e.preventDefault(); e.stopPropagation(); const oldZoom = this.currentZoom; if (e.deltaY < 0) { this.currentZoom = Math.min(this.MAX_ZOOM, this.currentZoom + this.ZOOM_STEP); } else { this.currentZoom = Math.max(this.MIN_ZOOM, this.currentZoom - this.ZOOM_STEP); } if (oldZoom !== this.currentZoom) handleZoomUpdate(); }, { passive: false }); container.addEventListener('click', () => { if (this.currentZoom !== this.DEFAULT_ZOOM) { this.currentZoom = this.DEFAULT_ZOOM; handleZoomUpdate(); } }); container.append(zoomIcon, zoomDisplay, tooltip); return container; } // --- 6. DOM監視 (DOM Observation) --- observeHeader() { const commentsElement = document.querySelector(this.containerSelector); if (!commentsElement) { if (this.pageType === 'shorts') { setTimeout(() => this.observeHeader(), 500); } return; } this.uiObserver = new MutationObserver(() => this.updateHeaderUI()); this.uiObserver.observe(commentsElement, { childList: true, subtree: true }); this.updateHeaderUI(); } updateHeaderUI() { if (document.getElementById(this.SCRIPT_ID)) return; const injectionTarget = document.querySelector(this.injectionTargetSelector); if (injectionTarget) { const zoomControls = this.createZoomControls(); if (this.pageType === 'shorts') { injectionTarget.insertAdjacentElement('afterend', zoomControls); } else { injectionTarget.parentElement.insertBefore(zoomControls, injectionTarget.nextSibling); } console.log(`[CommentZoom] UI injected for ${this.pageType}.`); } } } // --- 実行制御 (Execution Control) --- let zoomManager = null; function main() { if (zoomManager) { zoomManager.stop(); zoomManager = null; } (async () => { zoomManager = new CommentZoomManager(); await zoomManager.init(); })(); } window.addEventListener('yt-navigate-finish', main); main(); })();