[Asm] 纯文本查看 复制代码
// ==UserScript==
// [url=home.php?mod=space&uid=170990]@name[/url] FlowMouse - 心流鼠标
// [url=home.php?mod=space&uid=467642]@namespace[/url] [url]http://tampermonkey.net/[/url]
// [url=home.php?mod=space&uid=1248337]@version[/url] 1.0
// @description 一款追求极致流畅与隐私保护的鼠标手势脚本。支持手势导航、页面滚动、超级拖拽(文字/链接/图片),完全本地运行。
// [url=home.php?mod=space&uid=686208]@AuThor[/url] Hmily[LCG] (Ported by AI)
// [url=home.php?mod=space&uid=195849]@match[/url] *://*/*
// [url=home.php?mod=space&uid=609072]@grant[/url] GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_openInTab
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// =========================================================================
// 1. 常量与默认配置 (可在此修改)
// =========================================================================
const DEFAULT_CONFIG = {
enableTrail: true, // 开启轨迹
enableHUD: true, // 开启提示文字
enableTextDrag: true, // 开启文字拖拽
enableLinkDrag: true, // 开启链接拖拽
enableImageDrag: true, // 开启图片拖拽
searchEngine: 'google', // 默认搜索引擎
newTabUrl: 'https://www.google.com', // 【设置】新建标签页打开的网址 (因浏览器限制,无法打开原生NewTab)
trailColor: '#4285f4', // 轨迹颜色
trailWidth: 4, // 轨迹粗细
hudBgColor: '#000000', // 提示框背景
hudOpacity: 0.7 // 提示框透明度
};
const SEARCH_ENGINES = {
'google': 'https://www.google.com/search?q=',
'bing': 'https://www.bing.com/search?q=',
'baidu': 'https://www.baidu.com/s?wd='
};
// 【自定义手势区域】
const GESTURE_ACTIONS = {
'←': 'back', // 后退
'→': 'forward', // 前进
'↑': 'scrollUp', // 向上滚动
'↓': 'scrollDown', // 向下滚动
'↓→': 'closeTab', // 关闭当前页
'→↓': 'refresh', // 刷新
'↑←': 'scrollToTop', // 滚到顶部
'↑→': 'scrollToBottom', // 滚到底部
'→↑': 'newTab', // 新建标签页
'←↓': 'minimize', // 最小化/打开主页
'→←': 'restore' // 恢复
};
// 动作显示名称
const ACTION_NAMES = {
'back': '后退',
'forward': '前进',
'scrollUp': '向上滚动',
'scrollDown': '向下滚动',
'scrollToTop': '滚动到顶部',
'scrollToBottom': '滚动到底部',
'refresh': '刷新',
'closeTab': '关闭当前页',
'newTab': '新标签页',
'search': '搜索'
};
// =========================================================================
// 2. 配置管理
// =========================================================================
const Config = {
get: (key) => GM_getValue(key, DEFAULT_CONFIG[key]),
set: (key, value) => {
GM_setValue(key, value);
updateMenus();
},
getAll: () => {
let cfg = {};
for (let k in DEFAULT_CONFIG) cfg[k] = Config.get(k);
return cfg;
}
};
// =========================================================================
// 3. 视觉效果
// =========================================================================
class GestureVisualizer {
constructor() {
this.canvas = null;
this.ctx = null;
this.trail = [];
this.hud = null;
}
init() {
if (this.canvas) return;
this.canvas = document.createElement('canvas');
this.canvas.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 2147483647; opacity: 0;
transition: opacity 0.1s;
`;
document.documentElement.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
this.hud = document.createElement('div');
this.hud.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
padding: 12px 24px; border-radius: 8px;
color: #fff; font-family: sans-serif; font-size: 20px; font-weight: bold;
pointer-events: none; z-index: 2147483647; opacity: 0;
transition: opacity 0.2s; text-align: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.3); backdrop-filter: blur(2px);
`;
this.updateStyle();
document.documentElement.appendChild(this.hud);
window.addEventListener('resize', () => this.resize());
this.resize();
}
updateStyle() {
if (!this.hud) return;
const bg = Config.get('hudBgColor');
const op = Config.get('hudOpacity');
let r = parseInt(bg.slice(1, 3), 16), g = parseInt(bg.slice(3, 5), 16), b = parseInt(bg.slice(5, 7), 16);
this.hud.style.backgroundColor = `rgba(${r}, ${g}, ${b}, ${op})`;
}
resize() {
if (!this.canvas) return;
const dpr = window.devicePixelRatio || 1;
this.canvas.width = window.innerWidth * dpr;
this.canvas.height = window.innerHeight * dpr;
this.ctx.scale(dpr, dpr);
}
show() {
if (!Config.get('enableTrail')) return;
this.init();
this.canvas.style.opacity = '1';
this.trail = [];
}
addPoint(x, y) {
if (!Config.get('enableTrail')) return;
this.trail.push({ x, y });
this.draw();
}
draw() {
if (!this.ctx || this.trail.length < 2) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.beginPath();
this.ctx.strokeStyle = Config.get('trailColor');
this.ctx.lineWidth = Config.get('trailWidth');
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.moveTo(this.trail[0].x, this.trail[0].y);
for (let i = 1; i < this.trail.length; i++) {
this.ctx.lineTo(this.trail[i].x, this.trail[i].y);
}
this.ctx.stroke();
}
updateAction(text) {
if (!Config.get('enableHUD')) return;
this.init();
if (text) {
this.hud.textContent = text;
this.hud.style.opacity = '1';
} else {
this.hud.style.opacity = '0';
}
}
hide() {
if (this.canvas) {
this.canvas.style.opacity = '0';
setTimeout(() => { if (this.ctx) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); }, 100);
}
if (this.hud) this.hud.style.opacity = '0';
this.trail = [];
}
}
const visualizer = new GestureVisualizer();
// =========================================================================
// 4. 核心逻辑
// =========================================================================
let state = {
active: false,
startX: 0, startY: 0,
lastX: 0, lastY: 0,
pattern: [],
isDrag: false,
dragType: null,
dragContent: null,
preventMenu: false
};
const DISTANCE_THRESHOLD = 20;
function executeAction(actionCode, extraData) {
switch (actionCode) {
case 'back': window.history.back(); break;
case 'forward': window.history.forward(); break;
case 'scrollUp': window.scrollBy({ top: -window.innerHeight * 0.8, behavior: 'smooth' }); break;
case 'scrollDown': window.scrollBy({ top: window.innerHeight * 0.8, behavior: 'smooth' }); break;
case 'scrollToTop': window.scrollTo({ top: 0, behavior: 'smooth' }); break;
case 'scrollToBottom': window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); break;
case 'refresh': location.reload(); break;
case 'closeTab': window.close(); break;
case 'newTab': GM_openInTab(Config.get('newTabUrl'), { active: true }); break;
case 'search': {
let engine = Config.get('searchEngine');
let url = (SEARCH_ENGINES[engine] || SEARCH_ENGINES.google) + encodeURIComponent(extraData);
GM_openInTab(url, { active: true, insert: true });
break;
}
case 'openLink':
GM_openInTab(extraData, { active: false, insert: true });
break;
case 'openImage':
GM_openInTab(extraData, { active: true });
break;
}
}
function getDirection(dx, dy) {
if (Math.abs(dx) > Math.abs(dy)) return dx > 0 ? '→' : '←';
return dy > 0 ? '↓' : '↑';
}
function handleMove(currentX, currentY) {
if (!state.active) return;
visualizer.addPoint(currentX, currentY);
let dx = currentX - state.lastX;
let dy = currentY - state.lastY;
if (Math.sqrt(dx*dx + dy*dy) > DISTANCE_THRESHOLD) {
let dir = getDirection(dx, dy);
if (state.pattern[state.pattern.length - 1] !== dir) {
state.pattern.push(dir);
let patternStr = state.pattern.join('');
let actionName = '';
if (state.isDrag) {
if (patternStr.includes('→')) actionName = '搜索 / 打开';
} else {
let actionCode = GESTURE_ACTIONS[patternStr];
actionName = ACTION_NAMES[actionCode] || patternStr;
}
visualizer.updateAction(actionName);
}
state.lastX = currentX;
state.lastY = currentY;
}
}
// --- 强制拖拽逻辑 ---
document.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
let target = e.target;
// 【修复】确保 target 是元素节点
if (target.nodeType === 3) target = target.parentNode;
if (!target || !target.closest) return;
// 如果点击的是可编辑区域,不要强制开启拖拽,否则无法选字
if (target.isContentEditable || target.closest('[contenteditable="true"]')) return;
let depth = 0;
while (target && target !== document.body && depth < 5) {
if (target.getAttribute && target.getAttribute('draggable') === 'false') {
if (target.tagName === 'A' || target.tagName === 'IMG' ||
(window.getSelection().rangeCount > 0 && window.getSelection().containsNode(target, true))) {
target.setAttribute('draggable', 'true');
target.setAttribute('data-shoushi-modified', 'true');
}
}
target = target.parentElement;
depth++;
}
}, true);
const restoreDraggable = () => {
const modified = document.querySelectorAll('[data-shoushi-modified="true"]');
modified.forEach(el => {
el.setAttribute('draggable', 'false');
el.removeAttribute('data-shoushi-modified');
});
};
document.addEventListener('mouseup', restoreDraggable, true);
document.addEventListener('dragend', restoreDraggable, true);
// --- 鼠标手势事件 ---
document.addEventListener('mousedown', (e) => {
if (e.button === 2) {
state.active = false;
state.isDrag = false;
state.startX = e.clientX;
state.startY = e.clientY;
state.lastX = e.clientX;
state.lastY = e.clientY;
state.pattern = [];
state.preventMenu = false;
}
}, true);
document.addEventListener('mousemove', (e) => {
if (e.buttons !== 2 && !state.isDrag) return;
if (e.buttons === 2 && !state.active) {
let dist = Math.sqrt(Math.pow(e.clientX - state.startX, 2) + Math.pow(e.clientY - state.startY, 2));
if (dist > 10) {
state.active = true;
state.preventMenu = true;
visualizer.show();
visualizer.addPoint(state.startX, state.startY);
}
}
if (state.active) {
handleMove(e.clientX, e.clientY);
}
}, true);
document.addEventListener('mouseup', (e) => {
if (e.button === 2 && state.active) {
let patternStr = state.pattern.join('');
let action = GESTURE_ACTIONS[patternStr];
if (action) executeAction(action);
visualizer.hide();
state.active = false;
e.preventDefault();
e.stopPropagation();
} else if (e.button === 2) {
state.preventMenu = false;
}
}, true);
document.addEventListener('contextmenu', (e) => {
if (state.preventMenu) {
e.preventDefault();
e.stopPropagation();
state.preventMenu = false;
}
}, true);
// --- 超级拖拽事件 (核心修复) ---
document.addEventListener('dragstart', (e) => {
state.dragType = null;
state.dragContent = null;
state.pattern = [];
// 【修复】处理文本节点,确保 target 是 Element
let target = e.target;
if (target.nodeType === 3) target = target.parentNode;
// 安全检查
if (!target || typeof target.closest !== 'function') return;
if (Config.get('enableLinkDrag') && target.closest('a')) {
state.dragType = 'link';
state.dragContent = target.closest('a').href;
} else if (Config.get('enableImageDrag') && target.tagName === 'IMG') {
state.dragType = 'image';
state.dragContent = target.src;
} else if (Config.get('enableTextDrag')) {
let selection = window.getSelection().toString();
if (selection) {
state.dragType = 'text';
state.dragContent = selection;
}
}
if (state.dragType) {
state.isDrag = true;
state.startX = e.clientX;
state.startY = e.clientY;
state.lastX = e.clientX;
state.lastY = e.clientY;
e.dataTransfer.effectAllowed = 'copy';
// 兜底:如果没数据,强行塞一点,防止浏览器判定为空拖拽而禁止
if (!e.dataTransfer.types.length) {
e.dataTransfer.setData('text/plain', state.dragContent || 'FlowMouseDrag');
}
}
}, true); // 捕获阶段
// 【关键修复】使用捕获阶段(true)监听dragover,抢在网页JS之前告诉浏览器"这里可以Drop"
document.addEventListener('dragover', (e) => {
if (state.isDrag) {
e.preventDefault();
e.stopPropagation(); // 阻止事件冒泡给网页,防止网页的禁止逻辑生效
e.dataTransfer.dropEffect = 'copy';
if (!state.active) {
state.active = true;
visualizer.show();
visualizer.addPoint(state.startX, state.startY);
}
handleMove(e.clientX, e.clientY);
}
}, true); // ⚠️ 注意这里的 true,非常重要
document.addEventListener('dragend', (e) => {
if (state.isDrag && state.active) {
e.preventDefault();
visualizer.hide();
let patternStr = state.pattern.join('');
if (patternStr.includes('→') || patternStr.length === 0) {
if (state.dragType === 'text') executeAction('search', state.dragContent);
if (state.dragType === 'link') executeAction('openLink', state.dragContent);
if (state.dragType === 'image') executeAction('openImage', state.dragContent);
}
}
state.isDrag = false;
state.active = false;
}, true);
// 捕获阶段阻止 drop,防止网页原生的搜索或跳转干扰
document.addEventListener('drop', (e) => {
if (state.isDrag && state.active) {
e.preventDefault();
e.stopPropagation();
}
}, true);
// =========================================================================
// 5. 菜单系统
// =========================================================================
let menuIds = [];
function updateMenus() {
menuIds.forEach(id => GM_unregisterMenuCommand(id));
menuIds = [];
const add = (name, fn) => menuIds.push(GM_registerMenuCommand(name, fn));
add((Config.get('enableTrail') ? '✅' : '❌') + ' 启用轨迹', () => {
Config.set('enableTrail', !Config.get('enableTrail'));
});
add((Config.get('enableHUD') ? '✅' : '❌') + ' 启用提示', () => {
Config.set('enableHUD', !Config.get('enableHUD'));
});
add('🔍 搜索引擎: ' + (Config.get('searchEngine') === 'google' ? 'Google' : Config.get('searchEngine') === 'baidu' ? '百度' : 'Bing'), () => {
let engines = ['google', 'baidu', 'bing'];
let current = Config.get('searchEngine');
let next = engines[(engines.indexOf(current) + 1) % engines.length];
Config.set('searchEngine', next);
});
add((Config.get('enableTextDrag') ? '✅' : '❌') + ' 启用文字拖拽', () => {
Config.set('enableTextDrag', !Config.get('enableTextDrag'));
});
add((Config.get('enableLinkDrag') ? '✅' : '❌') + ' 启用链接拖拽', () => {
Config.set('enableLinkDrag', !Config.get('enableLinkDrag'));
});
add((Config.get('enableImageDrag') ? '✅' : '❌') + ' 启用图片拖拽', () => {
Config.set('enableImageDrag', !Config.get('enableImageDrag'));
});
}
updateMenus();
})();