// ==UserScript==
// @name 图寻音效
// @namespace https://gf.qytechs.cn/users/1179204
// @version 1.0.6
// @description 增加地图点击音效以及倒计时提醒音效,支持表情快捷键
// @author KaKa
// @icon https://www.svgrepo.com/show/521999/bell.svg
// @match *://tuxun.fun/*
// @exclude *://tuxun.fun/replay-pano?*
// @grant GM_addStyle
// @license BSD
// ==/UserScript==
(function() {
let google,map,streetViewPanorama,gameData,gameMode,gameId,roundState,currentRound,previousSize,isRecording,recordMode,events=[]
let replay_data=JSON.parse(localStorage.getItem('replay_data'))
if(!replay_data) replay_data={}
let videoIcon=`<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" fill="#fff" class="bi bi-camera-reels" viewBox="0 0 16 16">
<path d="M6 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0M1 3a2 2 0 1 0 4 0 2 2 0 0 0-4 0"/>
<path d="M9 6h.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 7.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 16H2a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zm6 8.73V7.27l-3.5 1.555v4.35zM1 8v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1"/>
<path d="M9 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6M7 3a2 2 0 1 1 4 0 2 2 0 0 1-4 0"/>
</svg>`
let iconUrl=svgToUrl(videoIcon)
const button = document.createElement('button');
button.id = 'replay';
let intervalId=setInterval(function(){
const streetViewContainer= document.getElementById('viewer')
const wrapper=document.querySelector('.wrapper___NMMQn')
if(streetViewContainer){
wrapper.appendChild(button)
clearInterval(intervalId)}
},500);
GM_addStyle(`
#replay {
position:absolute;
left:24px;
top:18%;
width: 40px;
height: 40px;
border: none;
border-radius:20px;
background-image: url('${iconUrl}');
background-size: auto;
background-position: center;
background-color:#000000;
background-repeat: no-repeat;
cursor: pointer;
color: white;
font-size: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
z-Index:9999
}
`);
button.onclick = () => {
if(!isRecording){
console.log('开始录制')
initObserver()
mapListener()
isRecording=true
const svgContent = videoIcon.replace(/fill="#fff"/g, 'fill="#D87A16"')
button.style.backgroundImage=`url(${svgToUrl(svgContent)})`
}
else{
isRecording=false
console.log('停止录制')
if(!replay_data[gameId])replay_data[gameId]={}
replay_data[gameId][currentRound]=events
localStorage.setItem('replay_data',JSON.stringify(replay_data))
events=[]
button.style.backgroundImage=`url('${iconUrl}')`
}
}
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
node.querySelectorAll && node.querySelectorAll('.countdownTimer___pqf8b').forEach(childNode => {
let intervalId = setInterval(function() {
const timeLeft = childNode.innerHTML.toString();
if (timeLeft.includes(':')) {
recordEvent('CountDown', timeLeft);
clearInterval(intervalId);
}
}, 100);
});
});
}
}
});
const config = { childList: true, subtree: true };
observer.observe(document.body, config);
function getSvContainer(){
const streetViewContainer= document.getElementById('viewer')
const keys = Object.keys(streetViewContainer)
const key = keys.find(key => key.startsWith("__reactFiber"))
const props = streetViewContainer[key]
streetViewPanorama=props.return.child.memoizedProps.children[1].props.googleMapInstance
gameData=props.return.return.return.return.return.memoizedState.next.next.memoizedState.current.gameData
gameMode=gameData.type
if(gameMode==='infinity'||gameMode==='challenge'){
recordMode=true
}
}
function svgToUrl(svgText) {
const svgBlob = new Blob([svgText], {type: 'image/svg+xml'});
const svgUrl = URL.createObjectURL(svgBlob);
return svgUrl;
}
function getMap(){
var mapContainer = document.getElementById('map')
const keys = Object.keys(mapContainer)
const key = keys.find(key => key.startsWith("__reactFiber$"))
const props = mapContainer[key]
const x = props.child.memoizedProps.value.map
map=x.getMap()
google=unsafeWindow.google
}
function mapListener() {
const mapContainer = document.querySelector('.maplibregl-canvas');
if (!mapContainer) {
console.error('Map container not found.');
return;
}
const observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style'&&isRecording) {
handleMapChange(mapContainer)
}
}
});
observer.observe(mapContainer, { attributes: true, attributeFilter: ['style'] });
}
function handleMapChange(mapContainer) {
const style = window.getComputedStyle(mapContainer);
const { width, height } = mapContainer.getBoundingClientRect();
const currentScreenWidth = window.innerWidth;
const widthRatio =Math.round((width / currentScreenWidth) * 100);
if (widthRatio===50&&roundState&&!recordMode){
recordEvent('RoundEnd')
roundState=false
if(!replay_data[gameId])replay_data[gameId]={}
replay_data[gameId][currentRound]=events
localStorage.setItem('replay_data',JSON.stringify(replay_data))
currentRound+=1
events=[]
}
else if (widthRatio!=50&&!roundState&&!recordMode){
roundState=true
}
if (widthRatio>=90&&previousSize<90&&recordMode){
recordEvent('RoundEnd')
roundState=false
if(!replay_data[gameId])replay_data[gameId]={}
replay_data[gameId][currentRound]=events
localStorage.setItem('replay_data',JSON.stringify(replay_data))
currentRound+=1
events=[]
}
else if (widthRatio<90&&previousSize>=90&&recordMode){
roundState=true
}
if (previousSize!=widthRatio&&roundState&&widthRatio!=50&&previousSize!=50){
recordEvent('MapSize',[Math.round(width),Math.round(height)]);
}
previousSize=widthRatio
}
function recordEvent(type,data) {
if(roundState&&isRecording){
events.push({time:new Date().getTime(),
type:type,
data:data});
}
}
function setObserver(panorama) {
let zoomLevel,bounds,mapCenter
if (gameData.status==='finish') return
panorama.addListener('position_changed', () => {
recordEvent('PanoPosition', panorama.getPano());
});
panorama.addListener('pov_changed', () => {
const pov = panorama.getPov()
const formattedPov = {
heading: pov.heading.toFixed(2),
pitch: pov.pitch.toFixed(2)
};
recordEvent('PanoPov',[formattedPov.heading,formattedPov.pitch]);
});
panorama.addListener('zoom_changed', () => {
recordEvent('PanoZoom', panorama.getZoom().toFixed(2));
});
map.on('moveend', () => {
bounds = map.getBounds();
mapCenter=bounds.getCenter();
recordEvent('MapView', [mapCenter.lat.toFixed(5),mapCenter.lng.toFixed(5)]);
});
map.on('zoom', (event) => {
zoomLevel = map.getZoom();
bounds = map.getBounds();
mapCenter = bounds.getCenter();
recordEvent('MapZoom', [mapCenter.lat.toFixed(5),mapCenter.lng.toFixed(5),zoomLevel]);
});
map.on('click', (event) => {
const { lngLat } = event;
recordEvent('Pin', [lngLat.lat.toFixed(5), lngLat.lng.toFixed(5)]);
});
}
function initObserver(){
try {
getSvContainer()
getMap()}
catch(error){
console.log('Failed to start listening')}
if(streetViewPanorama){
gameId=gameData.id||gameData.challengeId
currentRound=gameData.currentRound
var round
if (gameData.rounds.length===1&¤tRound!=1){
roundState=true
}
else{round=gameData.rounds[currentRound-1].endTime
if (round) roundState=false
else roundState=true}
setObserver(streetViewPanorama)}
}
let onKeyDown = (e) => {
if (e.key === 'v' || e.key === 'V') {
e.stopImmediatePropagation();
if(!isRecording){
console.log('开始录制')
initObserver()
mapListener()
isRecording=true
const svgContent = videoIcon.replace(/fill="#fff"/g, 'fill="#D87A16"')
button.style.backgroundImage=`url(${svgToUrl(svgContent)})`
}
else{
isRecording=false
console.log('停止录制')
if(!replay_data[gameId])replay_data[gameId]={}
replay_data[gameId][currentRound]=events
localStorage.setItem('replay_data',JSON.stringify(replay_data))
events=[]
button.style.backgroundImage=`url('${iconUrl}')`
}
}
}
document.addEventListener("keydown", onKeyDown);
})();