// ==UserScript==
// @name 虚拟地理位置
// @namespace https://github.com/LaLa-HaHa-Hei/
// @version 1.2.2
// @description 自定义浏览器中的地理位置
// @author 代码见三
// @license GPL-3.0-or-later
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
console.log('运行了 Virtual Geographic Location');
class FloatingButton {
// HTML 元素
menu = null;
button = null;
menuVisible = false;
// 拖动相关
isDragging = false;
isClick = false;
offsetY = 0;
// 数据
autoVirtual = false;
getSetValueFunction = null;
originalGetCurrentPosition = window.navigator.geolocation.getCurrentPosition
accuracy = 20000
latitude = 39.906217 // 纬度
longitude = 116.3912757 // 经度
constructor(accuracy, latitude, longitude, autoVirtual = false, enableJsIframeInjection = false, getSetValueFunction = null) {
if (document.body && document.body.getAttribute("vgl-injected-main")) {
console.log("vgl已经在此frame运行过");
return;
}
if (document.body) {
document.body.setAttribute("vgl-injected-main", "true");
}
this.accuracy = accuracy
this.latitude = latitude
this.longitude = longitude
this.autoVirtual = autoVirtual
this.getSetValueFunction = getSetValueFunction
const containerElement = this.injectHTML()
this.injectCSS()
this.button = containerElement.querySelector('#vgl-floating-button')
this.menu = containerElement.querySelector('#vgl-menu')
this.injectToJsframes = this.injectJsframes.bind(this)
this.startVirtual = this.startVirtual.bind(this);
this.stopVirtual = this.stopVirtual.bind(this);
this.showMenu = this.showMenu.bind(this);
this.hideMenu = this.hideMenu.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragMove = this.handleDragMove.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.bindDragEvents()
this.bindListeners()
if (autoVirtual)
this.startVirtual()
// 防止被覆盖,重新注入,每2秒检测一次,
const preventCoveredInterval = setInterval(() => {
if (!document.querySelector('#vgl-html')) {
console.log('检测到UI被移除,重新注入...');
const containerElement = this.injectHTML()
this.button = containerElement.querySelector('#vgl-floating-button')
this.menu = containerElement.querySelector('#vgl-menu')
if (autoVirtual)
this.startVirtual()
}
if (!document.querySelector('style#vgl-style')) {
console.log('选择样式被移除,重新注入...');
this.injectCSS();
}
}, 2 * 1000)
setTimeout(() => clearInterval(preventCoveredInterval), 10 * 1000); // 监听10秒后不再防止覆盖
// 注入所有js写入的iframe,带有src的ifrmae用match匹配
if (enableJsIframeInjection) {
this.injectJsframes()
setInterval(this.injectJsframes, 2 * 1000)
}
}
getValue(key, defaultValue) {
if (this.getSetValueFunction && this.getSetValueFunction.getValue) {
return this.getSetValueFunction.getValue(key, defaultValue)
}
}
setValue(key, value) {
if (this.getSetValueFunction && this.getSetValueFunction.setValue) {
this.getSetValueFunction.setValue(key, value)
}
}
injectJsframes() {
const iframes = document.querySelectorAll('iframe');
iframes.forEach(iframe => {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (!doc)
return;
// 避免重复注入
if (doc.body && doc.body.getAttribute("vgl-injected"))
return;
// 注入 vgl()
const script = doc.createElement('script');
script.type = 'text/javascript';
// 只深入一层iframe,因为一般只有一层
script.textContent = `
(function(){
var FloatingButton = ${FloatingButton.toString()};
new FloatingButton( ${this.accuracy}, ${this.latitude}, ${this.longitude}, ${this.autoVirtual}, false, null);
})()
`
doc.head.appendChild(script);
doc.body.setAttribute("vgl-injected", "true");
// console.log("已注入 iframe:", iframe);
console.log("已注入 iframe");
} catch (e) {
console.warn("无法注入 iframe(可能是跨域):", e);
}
})
}
startVirtual() {
this.accuracy = parseFloat(this.menu.querySelector('#vgl-accuracy-input').value)
this.latitude = parseFloat(this.menu.querySelector('#vgl-latitude-input').value)
this.longitude = parseFloat(this.menu.querySelector('#vgl-longitude-input').value)
this.setValue("vgl-accuracy", this.accuracy)
this.setValue("vgl-latitude", this.latitude)
this.setValue("vgl-longitude", this.longitude)
window.navigator.geolocation.getCurrentPosition = (successCallback, errorCallback, options) => {
const fakePosition = {
coords: {
accuracy: parseFloat(this.accuracy),
altitude: null,
altitudeAccuracy: null,
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
heading: null,
speed: null,
},
timestamp: Date.now(),
}
if (successCallback) {
successCallback(fakePosition);
}
}
}
stopVirtual() {
window.navigator.geolocation.getCurrentPosition = this.originalGetCurrentPosition
}
bindListeners() {
this.menu.querySelector('#vgl-confirm-button').addEventListener('click', () => {
this.hideMenu()
this.startVirtual()
})
this.menu.querySelector('#vgl-restore-button').addEventListener('click', () => {
this.hideMenu()
this.stopVirtual()
})
}
bindDragEvents() {
// 电脑端
document.addEventListener('mousedown', this.handleClickOutside);
this.button.addEventListener('mousedown', this.handleDragStart);
document.addEventListener('mousemove', this.handleDragMove);
document.addEventListener('mouseup', this.handleDragEnd);
// 手机端
document.addEventListener('touchstart', this.handleClickOutside, { passive: true });
this.button.addEventListener("touchstart", this.handleDragStart);
document.addEventListener("touchmove", this.handleDragMove);
document.addEventListener("touchend", this.handleDragEnd);
}
// 注入按钮和菜单
injectHTML() {
const injectedHTML = `
<button id="vgl-floating-button">虚拟位置</button>
<div id="vgl-menu">
<div class="vgl-menu-line">
<label for="vgl-accuracy-input">精度:</label>
<input type="number" id="vgl-accuracy-input" step="0.1" value="${this.accuracy}" />
</div>
<div class="vgl-menu-line">
<label for="vgl-latitude-input">纬度:</label>
<input type="number" id="vgl-latitude-input" step="0.1" value="${this.latitude}" />
</div>
<div class="vgl-menu-line">
<label for="vgl-longitude-input">经度:</label>
<input type="number" id="vgl-longitude-input" step="0.1" value="${this.longitude}" />
</div>
<div class="vgl-menu-line">
<button id="vgl-confirm-button">确定修改</button>
<button id="vgl-restore-button">取消虚拟</button>
</div>
</div>
`
const divElement = document.createElement('div')
divElement.innerHTML = injectedHTML
divElement.id = 'vgl-html'
document.body.appendChild(divElement)
return divElement
}
// 注入css
injectCSS() {
const injectedCSS = `
#vgl-floating-button {
position: fixed;
left: 0;
top: 42%;
z-index: 9999;
background: #007bff;
color: #fff;
border: none;
border-radius: 0 20px 20px 0;
padding: 6px 14px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
user-select: none;
transition: background 0.2s;
}
#vgl-floating-button:active {
background: #0056b3;
}
#vgl-menu {
display: none;
position: fixed;
left: 60px;
top: 40%;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 8px 0;
z-index: 10000;
}
.vgl-menu-line {
display: flex;
justify-content: center;
align-items: center;
margin: 8px 16px;
}
.vgl-menu-line button {
margin: 0 5px;
}
.vgl-menu-line input {
max-width: 180px;
box-sizing: border-box;
}
`
const styleElement = document.createElement('style')
styleElement.textContent = injectedCSS
styleElement.id = 'vgl-style'
document.head.appendChild(styleElement)
}
showMenu() {
const rect = this.button.getBoundingClientRect();
this.menu.style.top = rect.top + 'px';
this.menu.style.display = 'block';
this.menuVisible = true;
}
hideMenu() {
this.menu.style.display = 'none';
this.menuVisible = false;
}
handleClickOutside(event) {
if (!this.button.contains(event.target) && !this.menu.contains(event.target) && this.menuVisible)
this.hideMenu()
}
handleDragStart(event) {
this.isDragging = true;
this.isClick = true;
this.offsetY = (event.clientY || event.touches[0].clientY) - this.button.getBoundingClientRect().top;
if (this.menuVisible)
this.hideMenu();
event.preventDefault();
}
handleDragMove(event) {
if (this.isDragging) {
this.isClick = false;
let newTop = (event.clientY || event.touches[0].clientY) - this.offsetY;
this.button.style.top = newTop + 'px';
event.preventDefault();
}
}
handleDragEnd(event) {
this.isDragging = false;
if (this.isClick)
this.showMenu();
this.isClick = false;
}
}
let autoVirtual = GM_getValue("vgl-autoVirtual", false) // 打开页面后自动开启虚拟位置
let enableJsIframeInjection = GM_getValue("vgl-enableJsIframeInjection", false) // 打开页面后自动开启虚拟位置
let id1 = GM_registerMenuCommand(
"自动开始虚拟:" + (autoVirtual === true ? "已开" : "未开"),
menu1Click,
"a");
function menu1Click() {
GM_unregisterMenuCommand(id1)
autoVirtual = !autoVirtual
GM_setValue("vgl-autoVirtual", autoVirtual)
id1 = GM_registerMenuCommand(
"自动开始虚拟:" + (autoVirtual === true ? "已开" : "未开"),
menu1Click,
"a");
}
let id2 = GM_registerMenuCommand(
"包括js写入的iframe:" + (enableJsIframeInjection === true ? "已开" : "未开"),
menu2Click,
"i");
function menu2Click() {
GM_unregisterMenuCommand(id2)
enableJsIframeInjection = !enableJsIframeInjection
GM_setValue("vgl-enableJsIframeInjection", enableJsIframeInjection)
id2 = GM_registerMenuCommand(
"包括js写入的iframe:" + (enableJsIframeInjection === true ? "已开" : "未开"),
menu2Click,
"i");
}
new FloatingButton(
GM_getValue("vgl-accuracy", 20000),
GM_getValue("vgl-latitude", 39.906217),
GM_getValue("vgl-longitude", 116.3912757),
autoVirtual,
enableJsIframeInjection,
{
getValue: GM_getValue,
setValue: GM_setValue,
})
})();