// ==UserScript== // @name 虚拟地理位置 // @name:zh 虚拟地理位置 // @name:en Virtual Geographic Location // @namespace https://github.com/LaLa-HaHa-Hei/ // @version 1.1.1 // @description 自定义浏览器中的地理位置,在菜单中点击即可启用 // @author 代码见三 // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // ==/UserScript== (function () { 'use strict'; class VirtualLocationManager { originalGetCurrentPosition = navigator.geolocation.getCurrentPosition windowElement = null; headerElement = null; accuracyInputField = null; latitudeInputField = null; longitudeInputField = null; confirmButtonElement = null; styleElement = null; isDragging = false; offsetX = 0; offsetY = 0; createPopupWindow() { if (this.windowElement !== null) return; // 创建主容器 this.windowElement = document.createElement('div'); this.windowElement.id = 'VGL-popup-window'; // 创建头部 this.headerElement = document.createElement('header'); this.headerElement.id = 'VGL-popup-window-header'; this.headerElement.textContent = '虚拟地理位置'; // 创建主体内容 const main = document.createElement('main'); main.id = 'VGL-popup-window-main'; // 创建精度输入组 const accuracyGroup = document.createElement('div'); const accuracyLabel = document.createElement('label'); accuracyLabel.textContent = '精度'; this.accuracyInputField = document.createElement('input'); this.accuracyInputField.type = 'number'; this.accuracyInputField.id = 'VGL-accuracy-input'; accuracyGroup.appendChild(accuracyLabel); accuracyGroup.appendChild(this.accuracyInputField); // 创建纬度输入组 const latitudeGroup = document.createElement('div'); const latitudeLabel = document.createElement('label'); latitudeLabel.textContent = '纬度'; this.latitudeInputField = document.createElement('input'); this.latitudeInputField.type = 'number'; this.latitudeInputField.id = 'VGL-latitude-input'; latitudeGroup.appendChild(latitudeLabel); latitudeGroup.appendChild(this.latitudeInputField); // 创建经度输入组 const longitudeGroup = document.createElement('div'); const longitudeLabel = document.createElement('label'); longitudeLabel.textContent = '经度'; this.longitudeInputField = document.createElement('input'); this.longitudeInputField.type = 'number'; this.longitudeInputField.id = 'VGL-longitude-input'; longitudeGroup.appendChild(longitudeLabel); longitudeGroup.appendChild(this.longitudeInputField); // 创建按钮容器 const buttonGroup = document.createElement('div'); buttonGroup.style.display = 'flex'; buttonGroup.style.justifyContent = 'center'; this.confirmButtonElement = document.createElement('button'); this.confirmButtonElement.id = 'VGL-confirm-button'; this.confirmButtonElement.textContent = '确定'; buttonGroup.appendChild(this.confirmButtonElement); // 组装所有元素 main.appendChild(accuracyGroup); main.appendChild(latitudeGroup); main.appendChild(longitudeGroup); main.appendChild(buttonGroup); this.windowElement.appendChild(this.headerElement); this.windowElement.appendChild(main); // 添加到文档 document.body.appendChild(this.windowElement); // 调整为根据top而不是bottom定位,因为拖动逻辑是基于top的 // setTimeout(() => { // this.windowElement.style.bottom = 'auto' // this.windowElement.style.top = window.innerHeight - this.windowElement.clientHeight - 10 + "px" // }, 0); } injectPopupWindowCSS() { if (this.styleElement !== null) return; const injectedCSS = ` #VGL-popup-window { border: 1px solid #ccc; position: fixed; bottom: 10px; left: 10px; background-color: #fff; z-index: 2147483647; color: black; border-radius: 10px; overflow: hidden; } #VGL-popup-window-header { height: 30px; line-height: 30px; background-color: #409EFF; font-size: 18px; font-weight: 500; color: #fff; cursor: move; user-select: none; text-align: center; } #VGL-popup-window-main { padding: 20px; display: flex; flex-direction: column; gap: 10px; } #VGL-popup-window-main label { display: inline; } #VGL-popup-window-main input { height: 25px; width: 150px; border: 1px solid #ccc; } #VGL-confirm-button { width: 60px; height: 30px; background-color: #409EFF; color: #fff; font-size: 14px; font-weight: 500; border: none; cursor: pointer; border-radius: 10px; } `; this.styleElement = document.createElement('style'); this.styleElement.textContent = injectedCSS; this.styleElement.setAttribute('vgl-style', ''); document.head.appendChild(this.styleElement); } confirmButtonClick = (event) => { this.saveSettings(); window.navigator.geolocation.getCurrentPosition = (successCallback, errorCallback, options) => { const fakePosition = { coords: { accuracy: parseFloat(this.accuracyInputField.value), altitude: null, altitudeAccuracy: null, latitude: parseFloat(this.latitudeInputField.value), longitude: parseFloat(this.longitudeInputField.value), heading: null, speed: null, }, timestamp: Date.now(), } if (successCallback) { successCallback(fakePosition); } } alert("已设定虚拟地理位置"); }; handleDragStart = (event) => { const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; const rect = this.windowElement.getBoundingClientRect(); this.offsetX = clientX - rect.left; this.offsetY = clientY - rect.top; this.isDragging = true; event.preventDefault(); }; handleDragMove = (event) => { if (!this.isDragging) return; const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; const windowHeight = window.innerHeight; const elementHeight = this.windowElement.offsetHeight; const newX = clientX - this.offsetX; const newTop = clientY - this.offsetY; const newBottom = windowHeight - newTop - elementHeight; this.windowElement.style.left = `${newX}px`; this.windowElement.style.bottom = `${newBottom}px`; event.preventDefault(); }; handleDragEnd = (event) => { this.isDragging = false; }; bindEvents() { this.confirmButtonElement.addEventListener("click", this.confirmButtonClick); // 电脑端拖动 this.headerElement.addEventListener("mousedown", this.handleDragStart); document.addEventListener("mousemove", this.handleDragMove); document.addEventListener("mouseup", this.handleDragEnd); // 手机端拖动 this.headerElement.addEventListener("touchstart", this.handleDragStart); document.addEventListener("touchmove", this.handleDragMove); document.addEventListener("touchend", this.handleDragEnd); } unbindEvents() { this.confirmButtonElement.removeEventListener("click", this.confirmButtonClick); this.headerElement.removeEventListener("mousedown", this.handleDragStart); document.removeEventListener("mousemove", this.handleDragMove); document.removeEventListener("mouseup", this.handleDragEnd); this.headerElement.removeEventListener("touchstart", this.handleDragStart); document.removeEventListener("touchmove", this.handleDragMove); document.removeEventListener("touchend", this.handleDragEnd); } initializeInputFields() { this.accuracyInputField.value = GM_getValue("VGL-accuracy", 20000) this.latitudeInputField.value = GM_getValue("VGL-latitude", 0) this.longitudeInputField.value = GM_getValue("VGL-longitude", 0) } saveSettings() { GM_setValue("VGL-accuracy", this.accuracyInputField.value); GM_setValue("VGL-latitude", this.latitudeInputField.value); GM_setValue("VGL-longitude", this.longitudeInputField.value); } removePopupWindow() { if (this.windowElement !== null) { this.windowElement.remove(); this.windowElement = null; } } removePopupWindowCSS() { if (this.styleElement !== null) { this.styleElement.remove(); this.styleElement = null; } } cleanup() { this.unbindEvents(); this.removePopupWindow(); this.removePopupWindowCSS(); navigator.geolocation.getCurrentPosition = popupWindow.originalGetCurrentPosition } } let popupWindow = null; GM_registerMenuCommand("启动虚拟地理位置", () => { if (popupWindow === null) { popupWindow = new VirtualLocationManager(); } // 防止重复注入 else if (popupWindow.windowElement !== null) { alert("已启动虚拟地理位置,请勿重复启用"); return; } popupWindow.createPopupWindow(); popupWindow.injectPopupWindowCSS(); popupWindow.initializeInputFields(); popupWindow.bindEvents(); }, "h"); GM_registerMenuCommand("恢复原始状态", () => { if (popupWindow === null) { return; } popupWindow.cleanup(); }, "r"); })();