您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在Steam游戏/讨论组中添加格式帮助工具。支持拖拽定位、边缘检测和动画,防止UI冲突。
// ==UserScript== // @name Steam 格式助手 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 在Steam游戏/讨论组中添加格式帮助工具。支持拖拽定位、边缘检测和动画,防止UI冲突。 // @author Your name // @match https://store.steampowered.com/app/* // @match https://steamcommunity.com/app/*/discussions/* // @match https://steamcommunity.com/discussions/* // @match https://steamcommunity.com/groups/*/discussions/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // 检查当前页面是否是指定类型的讨论页面 function isDiscussionsPage() { const path = window.location.pathname; return path.includes('/discussions/') && (path.includes('/app/') || path.startsWith('/discussions/') || path.includes('/groups/')); } // 检查当前页面是否是游戏商店页面 function isGameStorePage() { return window.location.href.match(/https:\/\/store\.steampowered\.com\/app\/\d+/); } if (!isGameStorePage() && !isDiscussionsPage()) { return; } // 添加自定义样式 GM_addStyle(` #formatHelperBtn { position: fixed; /* 改为fixed以支持拖拽定位 */ z-index: 10000; background: linear-gradient( to bottom, #a4d007 5%, #536904 95%); color: #e5e4dc; border: none; border-radius: 2px; height: 28px; padding: 0 15px; font-size: 12px; font-family: "Motiva Sans", Arial, sans-serif; font-weight: normal; text-transform: none; text-shadow: 1px 1px 0px rgb(0 0 0 / 30%); cursor: grab; /* 初始光标为抓取手势 */ display: flex; align-items: center; justify-content: center; transition: box-shadow 0.2s ease; box-shadow: 0 0 10px rgba(0,0,0,0.5); user-select: none; /* 防止拖拽时选中文字 */ } #formatHelperBtn:hover { background: linear-gradient( to bottom, #b6d908 5%, #7a8b05 95%); } #formatHelperBtn:active { cursor: grabbing; } #formatHelperMenu { position: fixed; /* 改为fixed以支持动态定位 */ width: 280px; background: #1b2838; border-radius: 3px; box-shadow: 0 0 12px #000000; z-index: 9999; overflow: hidden; border: 1px solid #000; /* === 动画与可见性 === */ transition: opacity 0.2s ease, transform 0.2s ease; opacity: 0; transform: translateY(10px); pointer-events: none; } #formatHelperMenu.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } .format-section { padding: 10px; border-bottom: 1px solid #2a3f5a; } .format-section:last-child { border-bottom: none; } .format-section-title { color: #67c1f5; font-weight: bold; margin-bottom: 8px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-family: "Motiva Sans", Arial, sans-serif; font-size: 14px; } .format-section-content { display: none; flex-direction: column; gap: 5px; } .format-section.expanded .format-section-content { display: flex; } .format-option { padding: 8px; background: #2a3f5a; border-radius: 2px; color: #c6d4df; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px; position: relative; overflow: hidden; font-family: Arial, sans-serif; font-size: 13px; border: 1px solid transparent; } .format-option:hover { background: #3d5775; border: 1px solid #6d8ba5; } .format-option-preview { flex-grow: 1; } .copy-feedback { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(103, 193, 245, 0.9); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s; color: white; font-weight: bold; font-size: 12px; pointer-events: none; } .copy-feedback.show { opacity: 1; } /* --- 预览样式 --- */ .preview-h1 { font-size: 18px; font-weight: bold; color: #ffffff; font-family: "Motiva Sans", Arial, sans-serif; } .preview-h2 { font-size: 16px; font-weight: bold; color: #ffffff; font-family: "Motiva Sans", Arial, sans-serif; } .preview-h3 { font-size: 14px; font-weight: bold; color: #ffffff; font-family: "Motiva Sans", Arial, sans-serif; } .preview-bold { font-weight: bold; color: #ffffff; } .preview-underline { text-decoration: underline; color: #ffffff; } .preview-italic { font-style: italic; color: #ffffff; } .preview-strike { text-decoration: line-through; color: #ffffff; } .preview-spoiler { background: #000000; color: #000000; padding: 0 2px; } .preview-spoiler:hover { color: #ffffff; } .preview-link { color: #67c1f5; text-decoration: underline; } .preview-list-item { display: flex; align-items: center; gap: 5px; } .preview-list-bullet { display: inline-block; width: 5px; height: 5px; background: white; border-radius: 50%; } .preview-nlist-item { display: flex; align-items: center; gap: 5px; } .preview-quote { background: #2a3f5a; border-left: 3px solid #67c1f5; padding: 5px 8px; color: #c6d4df; } .preview-code { font-family: monospace; background: #2a3f5a; padding: 2px 4px; border-radius: 3px; color: #ffffff; } `); // 格式选项配置 (与v1.2相同) const formatOptions = [ { title: "标题", options: [ { name: "一级标题", format: "[h1]请输入标题文字[/h1]", preview: '<span class="preview-h1">一级标题</span>' }, { name: "二级标题", format: "[h2]请输入标题文字[/h2]", preview: '<span class="preview-h2">二级标题</span>' }, { name: "三级标题", format: "[h3]请输入标题文字[/h3]", preview: '<span class="preview-h3">三级标题</span>' } ] }, { title: "文本样式", options: [ { name: "粗体", format: "[b]请输入粗体文本[/b]", preview: '<span class="preview-bold">粗体文本</span>' }, { name: "下划线", format: "[u]请输入下划线文本[/u]", preview: '<span class="preview-underline">下划线文本</span>' }, { name: "斜体", format: "[i]请输入斜体文本[/i]", preview: '<span class="preview-italic">斜体文本</span>' }, { name: "删除线", format: "[strike]请输入删除文本[/strike]", preview: '<span class="preview-strike">删除线文本</span>' }, { name: "剧透", format: "[spoiler]请输入隐藏文本[/spoiler]", preview: '<span class="preview-spoiler">剧透(隐藏)文本</span>' } ] }, { title: "列表", options: [ { name: "无序列表", format: "[list]\n[*] 项目符号列表\n[*] 项目符号列表\n[/list]", preview: '<div class="preview-list-item"><span class="preview-list-bullet"></span><span>项目符号列表</span></div>' }, { name: "有序列表", format: "[olist]\n[*] 有序列表\n[*] 有序列表\n[/olist]", preview: '<div class="preview-nlist-item"><span>1.</span><span>有序列表</span></div>' } ] }, { title: "其他元素", options: [ { name: "链接", format: "[url=请输入链接地址]请输入网站名称[/url]", preview: '<span class="preview-link">网站链接</span>' }, { name: "引用", format: "[quote=请输入引用来源]请输入引用文本[/quote]", preview: '<div class="preview-quote">引用文本</div>' }, { name: "代码", format: "[code]请输入代码或等宽文本[/code]", preview: '<span class="preview-code">等宽字体,保留空格</span>' } ] } ]; /** * 更新菜单位置,实现边缘检测 * @param {HTMLElement} btn - 按钮元素 * @param {HTMLElement} menu - 菜单元素 */ function updateMenuPosition(btn, menu) { const btnRect = btn.getBoundingClientRect(); const menuHeight = menu.offsetHeight; const menuWidth = menu.offsetWidth; const gap = 10; // 按钮和菜单间的距离 let top, left; // 优先在按钮上方显示 if (btnRect.top > menuHeight + gap) { top = btnRect.top - menuHeight - gap; } else { // 上方空间不足,则在下方显示 top = btnRect.bottom + gap; } // 水平方向上,尽量让菜单右侧与按钮右侧对齐 left = btnRect.right - menuWidth; // 边缘检测:防止菜单超出屏幕左右边界 if (left < gap) { left = gap; } if (left + menuWidth > window.innerWidth - gap) { left = window.innerWidth - menuWidth - gap; } menu.style.top = `${top}px`; menu.style.left = `${left}px`; } /** * 使元素可拖拽并记忆位置 * @param {HTMLElement} btn - 要拖拽的按钮元素 */ function makeDraggable(btn) { // 读取保存的位置 const savedPos = GM_getValue('formatHelperBtnPos'); if (savedPos) { btn.style.top = savedPos.top; btn.style.left = savedPos.left; } else { // 默认初始位置 btn.style.bottom = '20px'; btn.style.right = '20px'; } let isDragging = false; let wasDragged = false; let offsetX, offsetY; btn.addEventListener('mousedown', (e) => { isDragging = true; wasDragged = false; // 如果是绝对定位,需要转换 btn.style.right = 'auto'; btn.style.bottom = 'auto'; const rect = btn.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!isDragging) return; wasDragged = true; let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; // 限制在视窗内 const rect = btn.getBoundingClientRect(); if (newLeft < 0) newLeft = 0; if (newTop < 0) newTop = 0; if (newLeft + rect.width > window.innerWidth) newLeft = window.innerWidth - rect.width; if (newTop + rect.height > window.innerHeight) newTop = window.innerHeight - rect.height; btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px'; } function onMouseUp() { isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); if (wasDragged) { // 保存最终位置 GM_setValue('formatHelperBtnPos', { top: btn.style.top, left: btn.style.left }); } } // 返回一个检查是否被拖拽的函数,用于区分点击和拖拽 return () => wasDragged; } // 创建帮助按钮和菜单 function createFormatHelper() { const btn = document.createElement('button'); btn.id = 'formatHelperBtn'; btn.textContent = '格式助手'; const menu = document.createElement('div'); menu.id = 'formatHelperMenu'; // 填充菜单内容... formatOptions.forEach(section => { const sectionEl = document.createElement('div'); sectionEl.className = 'format-section'; const titleEl = document.createElement('div'); titleEl.className = 'format-section-title'; titleEl.textContent = section.title; titleEl.innerHTML += '<span>▼</span>'; const contentEl = document.createElement('div'); contentEl.className = 'format-section-content'; section.options.forEach(option => { const optionEl = document.createElement('div'); optionEl.className = 'format-option'; optionEl.dataset.format = option.format; const feedbackEl = document.createElement('div'); feedbackEl.className = 'copy-feedback'; feedbackEl.textContent = '已复制!'; optionEl.innerHTML = `<div class="format-option-preview">${option.preview}</div>`; optionEl.appendChild(feedbackEl); optionEl.addEventListener('click', () => { GM_setClipboard(option.format, 'text'); feedbackEl.classList.add('show'); setTimeout(() => feedbackEl.classList.remove('show'), 1000); }); contentEl.appendChild(optionEl); }); titleEl.addEventListener('click', () => { const isExpanded = sectionEl.classList.toggle('expanded'); titleEl.querySelector('span').textContent = isExpanded ? '▲' : '▼'; }); sectionEl.appendChild(titleEl); sectionEl.appendChild(contentEl); menu.appendChild(sectionEl); }); document.body.appendChild(btn); document.body.appendChild(menu); const wasDraggedCheck = makeDraggable(btn); btn.addEventListener('click', (e) => { if (wasDraggedCheck()) return; // 如果是拖拽结束,则不触发点击 e.stopPropagation(); const isVisible = menu.classList.contains('visible'); if (!isVisible) { updateMenuPosition(btn, menu); // 显示前更新位置 menu.classList.add('visible'); // 默认展开第一个分类 if (!menu.querySelector('.expanded')) { const firstSection = menu.querySelector('.format-section'); if (firstSection) { firstSection.classList.add('expanded'); firstSection.querySelector('.format-section-title span').textContent = '▲'; } } } else { menu.classList.remove('visible'); } }); document.addEventListener('click', (e) => { if (!menu.contains(e.target) && e.target !== btn) { menu.classList.remove('visible'); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createFormatHelper); } else { createFormatHelper(); } })();