您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filter photos and videos in Google Maps contributions
// ==UserScript== // @name Google Maps Enhanced Photos // @namespace https://github.com/gncnpk/google-maps-enhanced-photos // @version 0.0.2 // @description Filter photos and videos in Google Maps contributions // @author Gavin Canon-Phratsachack (https://github.com/gncnpk) // @match https://www.google.com/maps/* // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com/maps // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; let isInit = false; let oldHref = document.location.href; // Store observers for cleanup let containerObserver = null; // UI elements let popup, filterContainer, placeFilterContainer; let autoLoadEnabled = false; let currentFilter = 'all'; // 'all', 'photos', 'videos' let currentPlaceFilter = 'all'; // 'all' or specific place name // Container references let contentContainer = null; let scrollContainer = null; // Most viewed element tracking let mostViewedElement = null; let mostViewedCount = 0; // Place tracking let availablePlaces = new Set(); // Multi-language photo and video keywords const PHOTO_KEYWORDS = [ // English 'photo', 'picture', 'image', 'pic', // German 'foto', 'bild', // French 'photo', 'image', // Spanish 'foto', 'imagen', // Italian 'foto', 'immagine', // Portuguese 'foto', 'imagem', // Dutch 'foto', 'afbeelding', // Russian 'фото', // Japanese '写真', 'しゃしん', // Chinese (Simplified & Traditional) '照片', '相片', // Korean '사진', // Arabic 'صورة', // Polish 'zdjęcie', 'foto', // Czech 'fotografie', 'obrázek', // Swedish 'foto', 'bild', // Norwegian 'foto', 'bilde', // Danish 'foto', 'billede', // Finnish 'kuva', 'valokuva', // Hungarian 'fotó', 'kép', // Turkish 'fotoğraf', 'resim', // Hebrew 'תמונה' ]; const VIDEO_KEYWORDS = [ // English 'video', 'movie', 'clip', // German 'video', 'film', // French 'vidéo', 'film', // Spanish 'vídeo', 'video', 'película', // Italian 'video', 'filmato', // Portuguese 'vídeo', 'filme', // Dutch 'video', 'film', // Russian 'видео', // Japanese 'ビデオ', '動画', 'びでお', 'どうが', // Chinese (Simplified & Traditional) '视频', '影片', '錄像', // Korean '비디오', '영상', // Arabic 'فيديو', // Polish 'wideo', 'film', // Czech 'video', 'film', // Swedish 'video', 'film', // Norwegian 'video', 'film', // Danish 'video', 'film', // Finnish 'video', 'elokuva', // Hungarian 'videó', 'film', // Turkish 'video', 'film', // Hebrew 'וידאו', 'סרטון' ]; // Function to check if an aria-label contains photo keywords function hasPhotoKeyword(ariaLabel) { if (!ariaLabel) return false; const lowerLabel = ariaLabel.toLowerCase(); return PHOTO_KEYWORDS.some(keyword => lowerLabel.includes(keyword.toLowerCase()) ); } // Function to check if an aria-label contains video keywords function hasVideoKeyword(ariaLabel) { if (!ariaLabel) return false; const lowerLabel = ariaLabel.toLowerCase(); return VIDEO_KEYWORDS.some(keyword => lowerLabel.includes(keyword.toLowerCase()) ); } // Inject CSS const style = document.createElement('style'); style.textContent = ` .photo-video-popup { position: fixed; top: 10px; right: 10px; background: #fff; border: 1px solid #ccc; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); padding: 15px; z-index: 9999; width: 250px; font-family: Arial, sans-serif; max-height: 80vh; overflow-y: auto; } .filter-btn { background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 8px 12px; margin: 4px 2px; cursor: pointer; font-size: 12px; width: calc(50% - 4px); display: inline-block; text-align: center; } .filter-btn.active { background: #4285f4; color: white; border-color: #4285f4; } .place-filter-select { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; margin-top: 5px; background: white; } .auto-load-btn, .scroll-to-most-viewed-btn { background: #34a853; border: none; border-radius: 4px; color: white; padding: 8px 12px; font-size: 12px; cursor: pointer; width: 100%; margin-top: 8px; } .auto-load-btn.enabled { background: #ea4335; } .scroll-to-most-viewed-btn { background: #ff9800; } .scroll-to-most-viewed-btn:hover { background: #f57c00; } .scroll-to-most-viewed-btn:disabled { background: #ccc; cursor: not-allowed; } .drag-handle { cursor: move; font-weight: bold; margin-bottom: 10px; padding: 5px; border-bottom: 1px solid #eee; } .stats { line-height: 1.25em; } .most-viewed-info { font-size: 11px; color: #666; margin-top: 5px; padding: 5px; background: #f9f9f9; border-radius: 3px; } .filter-section { margin-bottom: 15px; } .filter-header { font-size: 14px; font-weight: bold; margin-bottom: 8px; } `; document.head.appendChild(style); // Find content container (photos/videos grid) function findContentContainer() { // Look for the container that holds photos/videos const containers = document.querySelectorAll('.m6QErb.XiKgde'); for (let container of containers) { // Check if container has photo/video items using our keyword functions const mediaElements = container.querySelectorAll('.xUc6Hf[aria-label]'); const hasMedia = Array.from(mediaElements).some(el => hasPhotoKeyword(el.getAttribute('aria-label')) || hasVideoKeyword(el.getAttribute('aria-label')) ); if (hasMedia && container.children.length > 5) { // Reasonable threshold return container; } } return null; } // Find scroll container function findScrollContainer() { if (contentContainer) { let container = contentContainer; while (container && container !== document.body) { const style = getComputedStyle(container); if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && container.scrollHeight > container.clientHeight) { return container; } container = container.parentElement; } } // Fallback: find any scrollable container with media const scrollableElements = Array.from(document.querySelectorAll('*')).filter(el => { const style = getComputedStyle(el); const hasScrollableY = (style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight; if (!hasScrollableY) return false; // Check for media using our keyword functions const mediaElements = el.querySelectorAll('.xUc6Hf[aria-label]'); return Array.from(mediaElements).some(element => hasPhotoKeyword(element.getAttribute('aria-label')) || hasVideoKeyword(element.getAttribute('aria-label')) ); }); return scrollableElements.length > 0 ? scrollableElements[0] : null; } // Extract place name from a post element function getPlaceFromPost(postElement) { const placeElement = postElement.querySelector(".UwKPnd .fgD3Vc.fontTitleSmall"); return placeElement ? placeElement.innerText.trim() : null; } // Scan all posts and collect unique places function updateAvailablePlaces() { if (!contentContainer) return; const newPlaces = new Set(); Array.from(contentContainer.children).forEach(item => { const place = getPlaceFromPost(item); if (place) { newPlaces.add(place); } }); // Check if places have changed const placesChanged = newPlaces.size !== availablePlaces.size || [...newPlaces].some(place => !availablePlaces.has(place)); availablePlaces = newPlaces; // Update place filter dropdown if places changed if (placesChanged) { updatePlaceFilterDropdown(); } } // Update the place filter dropdown function updatePlaceFilterDropdown() { const placeSelect = popup?.querySelector('.place-filter-select'); if (!placeSelect) return; const currentValue = placeSelect.value; placeSelect.innerHTML = '<option value="all">All Places</option>'; const sortedPlaces = [...availablePlaces].sort(); sortedPlaces.forEach(place => { const option = document.createElement('option'); option.value = place; option.textContent = place; placeSelect.appendChild(option); }); // Restore previous selection if it still exists if (currentValue && availablePlaces.has(currentValue)) { placeSelect.value = currentValue; currentPlaceFilter = currentValue; } else if (currentValue !== 'all') { // Reset to 'all' if previous selection no longer exists currentPlaceFilter = 'all'; placeSelect.value = 'all'; } } // Find element with most views (respecting current filters) function findMostViewedElement() { if (!contentContainer) { mostViewedElement = null; mostViewedCount = 0; return; } const viewElements = document.querySelectorAll("div.WqkvRc.fontBodySmall.BfMscf > div.HtPsUd"); if (viewElements.length === 0) { mostViewedElement = null; mostViewedCount = 0; return; } let maxViews = 0; let maxElement = null; viewElements.forEach(element => { // Find the parent post container let postContainer = element; while (postContainer && !postContainer.parentElement?.classList.contains('m6QErb')) { postContainer = postContainer.parentElement; } // Skip if we can't find the post container or if it's hidden by the filter if (!postContainer || getComputedStyle(postContainer).display === 'none') { return; } const viewText = element.innerText.replace(/,/g, ''); const views = parseInt(viewText); if (!isNaN(views) && views > maxViews) { maxViews = views; maxElement = element; } }); mostViewedElement = maxElement; mostViewedCount = maxViews; } // Scroll to most viewed element function scrollToMostViewed() { if (!mostViewedElement || !scrollContainer) { return; } // Find the parent post container let postContainer = mostViewedElement; while (postContainer && !postContainer.matches('.m6QErb.XiKgde > *')) { postContainer = postContainer.parentElement; } if (postContainer) { // Scroll the element into view postContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add temporary highlight effect const originalTransition = postContainer.style.transition; const originalBackground = postContainer.style.backgroundColor; postContainer.style.transition = 'background-color 0.3s ease'; postContainer.style.backgroundColor = '#fff3cd'; setTimeout(() => { postContainer.style.backgroundColor = originalBackground; setTimeout(() => { postContainer.style.transition = originalTransition; }, 300); }, 1500); } } // Filter content based on selection function filterContent() { if (!contentContainer) return; // Scroll to top when filtering if (scrollContainer) { scrollContainer.scrollTop = 0; } Array.from(contentContainer.children).forEach(item => { // Use our keyword functions to detect photos and videos const mediaElements = item.querySelectorAll('.xUc6Hf[aria-label]'); const hasPhoto = Array.from(mediaElements).some(el => hasPhotoKeyword(el.getAttribute('aria-label')) ); const hasVideo = Array.from(mediaElements).some(el => hasVideoKeyword(el.getAttribute('aria-label')) ); const itemPlace = getPlaceFromPost(item); let visible = true; // Apply media type filter switch (currentFilter) { case 'photos': visible = hasPhoto; break; case 'videos': visible = hasVideo; break; case 'all': default: visible = true; break; } // Apply place filter if (visible && currentPlaceFilter !== 'all') { visible = itemPlace === currentPlaceFilter; } item.style.display = visible ? '' : 'none'; }); updateStats(); } // Update statistics function updateStats() { if (!contentContainer) return; const allItems = Array.from(contentContainer.children); const visibleItems = allItems.filter(item => getComputedStyle(item).display !== 'none'); let photoCount = 0; let videoCount = 0; visibleItems.forEach(item => { const mediaElements = item.querySelectorAll('.xUc6Hf[aria-label]'); Array.from(mediaElements).forEach(element => { const ariaLabel = element.getAttribute('aria-label'); if (hasPhotoKeyword(ariaLabel)) { photoCount++; } else if (hasVideoKeyword(ariaLabel)) { videoCount++; } }); }); // Update available places updateAvailablePlaces(); // Find most viewed element findMostViewedElement(); const statsDiv = popup.querySelector('.stats'); if (statsDiv) { let statsText = `${visibleItems.length} posts<br>${photoCount} photos<br>${videoCount} videos`; if (currentPlaceFilter !== 'all') { statsText += `<br>Place: ${currentPlaceFilter}`; } statsDiv.innerHTML = statsText; } // Update most viewed info const mostViewedInfo = popup.querySelector('.most-viewed-info'); const scrollBtn = popup.querySelector('.scroll-to-most-viewed-btn'); if (mostViewedInfo && scrollBtn) { if (mostViewedElement && mostViewedCount > 0) { mostViewedInfo.innerHTML = `Most viewed: ${mostViewedCount.toLocaleString()} views`; scrollBtn.disabled = false; scrollBtn.textContent = 'Go to Most Viewed'; } else { mostViewedInfo.innerHTML = 'No view data found'; scrollBtn.disabled = true; scrollBtn.textContent = 'No Views Found'; } } // Auto-scroll if enabled if (autoLoadEnabled && scrollContainer) { setTimeout(() => { scrollContainer.scrollTop = scrollContainer.scrollHeight; }, 100); } } // Toggle auto load function toggleAutoLoad() { autoLoadEnabled = !autoLoadEnabled; const autoLoadBtn = popup.querySelector('.auto-load-btn'); if (autoLoadBtn) { autoLoadBtn.textContent = autoLoadEnabled ? 'Disable Auto Load' : 'Enable Auto Load'; autoLoadBtn.classList.toggle('enabled', autoLoadEnabled); } if (autoLoadEnabled && scrollContainer) { scrollContainer.scrollTop = scrollContainer.scrollHeight; } } // Make element draggable function makeDraggable(el, handleSelector) { const handle = el.querySelector(handleSelector); if (!handle) return; handle.style.cursor = 'move'; let offsetX = 0, offsetY = 0; handle.addEventListener('pointerdown', e => { const r = el.getBoundingClientRect(); el.style.left = `${r.left}px`; el.style.top = `${r.top}px`; el.style.right = 'auto'; el.style.transform = 'none'; offsetX = e.clientX - r.left; offsetY = e.clientY - r.top; el.setPointerCapture(e.pointerId); e.preventDefault(); }); el.addEventListener('pointermove', e => { if (!el.hasPointerCapture(e.pointerId)) return; el.style.left = `${e.clientX - offsetX}px`; el.style.top = `${e.clientY - offsetY}px`; }); ['pointerup', 'pointercancel'].forEach(evt => { el.addEventListener(evt, e => { if (el.hasPointerCapture(e.pointerId)) { el.releasePointerCapture(e.pointerId); } }); }); } // Create popup UI function createPopup() { popup = document.createElement('div'); popup.className = 'photo-video-popup'; // Drag handle / stats const statsDiv = document.createElement('div'); statsDiv.className = 'stats drag-handle'; statsDiv.textContent = 'Photo/Video Filter'; popup.appendChild(statsDiv); // Media type filter section const mediaFilterSection = document.createElement('div'); mediaFilterSection.className = 'filter-section'; const mediaFilterHeader = document.createElement('div'); mediaFilterHeader.className = 'filter-header'; mediaFilterHeader.textContent = 'Filter by Media Type'; mediaFilterSection.appendChild(mediaFilterHeader); filterContainer = document.createElement('div'); const filters = [ { id: 'all', label: 'All' }, { id: 'photos', label: 'Photos' }, { id: 'videos', label: 'Videos' } ]; filters.forEach(filter => { const btn = document.createElement('button'); btn.className = 'filter-btn'; btn.textContent = filter.label; if (filter.id === currentFilter) { btn.classList.add('active'); } btn.addEventListener('click', () => { // Remove active class from all buttons filterContainer.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); // Add active class to clicked button btn.classList.add('active'); currentFilter = filter.id; filterContent(); }); filterContainer.appendChild(btn); }); mediaFilterSection.appendChild(filterContainer); popup.appendChild(mediaFilterSection); // Place filter section const placeFilterSection = document.createElement('div'); placeFilterSection.className = 'filter-section'; const placeFilterHeader = document.createElement('div'); placeFilterHeader.className = 'filter-header'; placeFilterHeader.textContent = 'Filter by Place'; placeFilterSection.appendChild(placeFilterHeader); const placeSelect = document.createElement('select'); placeSelect.className = 'place-filter-select'; placeSelect.innerHTML = '<option value="all">All Places</option>'; placeSelect.addEventListener('change', (e) => { currentPlaceFilter = e.target.value; filterContent(); }); placeFilterSection.appendChild(placeSelect); popup.appendChild(placeFilterSection); // Most viewed info const mostViewedInfo = document.createElement('div'); mostViewedInfo.className = 'most-viewed-info'; mostViewedInfo.textContent = 'Searching for views...'; popup.appendChild(mostViewedInfo); // Scroll to most viewed button const scrollToMostViewedBtn = document.createElement('button'); scrollToMostViewedBtn.className = 'scroll-to-most-viewed-btn'; scrollToMostViewedBtn.textContent = 'Go to Most Viewed'; scrollToMostViewedBtn.addEventListener('click', scrollToMostViewed); popup.appendChild(scrollToMostViewedBtn); // Auto load button const autoLoadBtn = document.createElement('button'); autoLoadBtn.className = 'auto-load-btn'; autoLoadBtn.textContent = 'Enable Auto Load'; autoLoadBtn.addEventListener('click', toggleAutoLoad); popup.appendChild(autoLoadBtn); document.body.appendChild(popup); makeDraggable(popup, '.drag-handle'); } // Watch for container changes function setupContainerWatcher() { let retryCount = 0; const maxRetries = 30; function trySetup() { console.log(`Looking for content container (attempt ${retryCount + 1}/${maxRetries})`); const container = findContentContainer(); if (!container) { retryCount++; if (retryCount < maxRetries) { setTimeout(trySetup, 1000); } else { console.warn('Could not find content container'); } return; } console.log('Found content container:', container); contentContainer = container; scrollContainer = findScrollContainer(); if (scrollContainer) { console.log('Found scroll container:', scrollContainer); } // Initial update updateStats(); filterContent(); // Watch for changes containerObserver = new MutationObserver(() => { updateStats(); if (currentFilter !== 'all' || currentPlaceFilter !== 'all') { filterContent(); } }); containerObserver.observe(contentContainer, { childList: true, subtree: true }); } trySetup(); } // Cleanup function function cleanup() { console.log('Cleaning up Photo/Video Filter'); if (popup && popup.parentNode) { popup.parentNode.removeChild(popup); popup = null; } if (containerObserver) { containerObserver.disconnect(); containerObserver = null; } contentContainer = null; scrollContainer = null; filterContainer = null; placeFilterContainer = null; autoLoadEnabled = false; currentFilter = 'all'; currentPlaceFilter = 'all'; mostViewedElement = null; mostViewedCount = 0; availablePlaces.clear(); isInit = false; } // Check if we should initialize function checkInitState() { const shouldInit = window.location.href.includes('/contrib/') && window.location.href.includes('/photos'); if (shouldInit && !isInit) { console.log('Initializing Photo/Video Filter'); isInit = true; createPopup(); setupContainerWatcher(); } else if (!shouldInit && isInit) { cleanup(); } } // Initialize on DOM ready and watch for URL changes document.addEventListener("DOMContentLoaded", function() { const bodyList = document.querySelector('body'); const observer = new MutationObserver(function(mutations) { if (oldHref !== document.location.href) { oldHref = document.location.href; checkInitState(); } }); observer.observe(bodyList, { childList: true, subtree: true }); checkInitState(); }); })();