虚拟地理位置
// ==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");
})();