您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds filters to show/hide crime scenario cards and disable low-success chance join buttons
// ==UserScript== // @name [TORN] OC 2.0 Priority Helper // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description Adds filters to show/hide crime scenario cards and disable low-success chance join buttons // @match https://www.torn.com/factions.php* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // Constants for localStorage keys const STORAGE_KEYS = { SHOW: 'tornCrimeFilter_show', LEVEL: 'tornCrimeFilter_level', POSITION_X: 'tornCrimeFilter_posX', POSITION_Y: 'tornCrimeFilter_posY', IS_COLLAPSED: 'tornCrimeFilter_collapsed', HIGHLIGHT_PRIORITY: 'tornCrimeFilter_highlightPriority', MIN_SUCCESS: 'tornCrimeFilter_minSuccess' }; // Default settings const DEFAULT_SETTINGS = { show: true, level: 1, positionX: 0, positionY: 0, isCollapsed: false, highlightPriority: true, minSuccess: 62 }; // Priority colors const PRIORITY_COLORS = { waiting: 'rgba(83, 152, 237, 0.4)', // Blue - #1 Waiting empty: 'rgba(76, 175, 80, 0.4)', // Green - #2 Empty nobodyQueueing: 'rgba(255, 152, 0, 0.4)', // Orange - #3 Nobody queueing others: 'rgba(0, 0, 0, 0)' // Transparent - #4 All others }; // Load saved settings or use defaults function loadSettings() { return { show: localStorage.getItem(STORAGE_KEYS.SHOW) !== 'false', level: parseInt(localStorage.getItem(STORAGE_KEYS.LEVEL)) || DEFAULT_SETTINGS.level, positionX: parseInt(localStorage.getItem(STORAGE_KEYS.POSITION_X)) || DEFAULT_SETTINGS.positionX, positionY: parseInt(localStorage.getItem(STORAGE_KEYS.POSITION_Y)) || DEFAULT_SETTINGS.positionY, isCollapsed: localStorage.getItem(STORAGE_KEYS.IS_COLLAPSED) === 'true', highlightPriority: localStorage.getItem(STORAGE_KEYS.HIGHLIGHT_PRIORITY) !== 'false', minSuccess: parseInt(localStorage.getItem(STORAGE_KEYS.MIN_SUCCESS)) || DEFAULT_SETTINGS.minSuccess }; } // Save settings to localStorage function saveSettings(settings) { Object.entries(settings).forEach(([key, value]) => { localStorage.setItem(STORAGE_KEYS[key.toUpperCase()], value); }); } // Ensure position is within viewport bounds function keepInBounds(element) { const rect = element.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Extract transformed position const transformMatrix = window.getComputedStyle(element).transform; let currentX = 0; let currentY = 0; if (transformMatrix && transformMatrix !== 'none') { const matrixValues = transformMatrix.match(/matrix.*\((.+)\)/)[1].split(', '); currentX = parseInt(matrixValues[4]) || 0; currentY = parseInt(matrixValues[5]) || 0; } let newX = currentX; let newY = currentY; if (rect.right > viewportWidth) { newX = viewportWidth - rect.width; } if (rect.left < 0) { newX = 0; } if (rect.bottom > viewportHeight) { newY = viewportHeight - rect.height; } if (rect.top < 0) { newY = 0; } return { x: newX, y: newY }; } // Get slot status - returns one of: 'completed', 'inProgress', 'notStarted', 'empty' function getSlotStatus(slotElement) { // Check if the slot is empty const svgElement = slotElement.querySelector('.slotIcon___VVnQy svg'); if (svgElement) return 'empty'; // Get the planning element const planningElement = slotElement.querySelector('.planning___CjB09'); if (!planningElement) return 'unknown'; // Get the conic-gradient style to determine progress const style = planningElement.getAttribute('style'); if (!style) return 'unknown'; // Extract the degrees value from the conic-gradient const match = style.match(/conic-gradient\([^,]+,\s*[^,]+\s+([0-9.]+)deg/); if (!match) return 'unknown'; const degrees = parseFloat(match[1]); if (degrees >= 360) return 'completed'; if (degrees > 0) return 'inProgress'; return 'notStarted'; } // Check if a slot is filled (has a participant) function isSlotFilled(slotElement) { return slotElement.querySelector('.validSlot___n6ueL') !== null; } // Get the success chance of a slot (0-100) function getSuccessChance(slotElement) { const successChanceEl = slotElement.querySelector('.successChance___ddHsR'); if (!successChanceEl) return 0; const chanceText = successChanceEl.textContent.trim(); return parseInt(chanceText, 10) || 0; } // Process join buttons - disable if success chance is too low function processJoinButtons(minSuccess) { let disabledCount = 0; let totalButtons = 0; document.querySelectorAll('.joinButton___Ikoyy').forEach(button => { // Find the parent slot element to check the success chance const slotElement = button.closest('.wrapper___Lpz_D'); if (!slotElement) return; const successChance = getSuccessChance(slotElement); totalButtons++; // Apply disabled state based on success chance if (successChance < minSuccess) { button.disabled = true; button.title = `Success chance (${successChance}) is below minimum (${minSuccess})`; disabledCount++; // Add visual indicator button.style.opacity = '0.6'; button.style.position = 'relative'; button.style.cursor = 'not-allowed'; // Check if we need to add the strike-through if (!button.querySelector('.strike-through')) { const strikeThrough = document.createElement('div'); strikeThrough.className = 'strike-through'; strikeThrough.style.position = 'absolute'; strikeThrough.style.top = '50%'; strikeThrough.style.left = '0'; strikeThrough.style.right = '0'; strikeThrough.style.height = '1px'; strikeThrough.style.backgroundColor = 'red'; strikeThrough.style.transform = 'rotate(-10deg)'; button.appendChild(strikeThrough); } } else { // Ensure button is enabled and clean button.disabled = false; button.title = ''; button.style.opacity = ''; button.style.cursor = ''; // Remove strike-through if exists const strikeThrough = button.querySelector('.strike-through'); if (strikeThrough) { strikeThrough.remove(); } } }); return { disabledCount, totalButtons }; } // Process crime cards, add badges and apply highlighting function processCrimeCards(highlightPriority, minSuccess) { const priorityCounts = { waiting: 0, empty: 0, nobodyQueueing: 0, others: 0 }; document.querySelectorAll('.wrapper___U2Ap7').forEach(card => { // Add participant badge if not exists if (!card.querySelector('.participant-badge')) { // Get the participants container const participantsContainer = card.querySelector('.wrapper___g3mPt'); if (participantsContainer) { // Count filled slots and total slots const allSlots = participantsContainer.querySelectorAll('.wrapper___Lpz_D'); const filledSlots = participantsContainer.querySelectorAll('.validSlot___n6ueL'); // Create the badge const badge = document.createElement('div'); badge.className = 'participant-badge'; badge.style.position = 'absolute'; badge.style.top = '0'; badge.style.right = '0'; badge.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; badge.style.color = '#fff'; badge.style.padding = '3px 8px'; badge.style.borderRadius = '0 0 0 8px'; badge.style.fontSize = '12px'; badge.style.fontWeight = 'bold'; badge.style.zIndex = '10'; badge.style.textShadow = '1px 1px 1px rgba(0,0,0,0.5)'; // Set the badge text badge.textContent = `${filledSlots.length}/${allSlots.length} slots`; // Add the badge to the card card.querySelector('.contentLayer___IYFdz').appendChild(badge); } } // Add or update priority badge if (highlightPriority) { // First, remove any existing priority badge const existingBadge = card.querySelector('.priority-badge'); if (existingBadge) { existingBadge.remove(); } // Remove previous highlight card.style.backgroundColor = ''; card.style.boxShadow = ''; // Get the participants container and analyze slots const participantsContainer = card.querySelector('.wrapper___g3mPt'); if (participantsContainer) { const slots = Array.from(participantsContainer.querySelectorAll('.wrapper___Lpz_D')); const filledSlots = slots.filter(slot => isSlotFilled(slot)); const emptySlots = slots.filter(slot => !isSlotFilled(slot)); // Get status of each slot const slotStatuses = slots.map(slot => ({ element: slot, filled: isSlotFilled(slot), status: getSlotStatus(slot) })); let priorityType = ''; let priorityLabel = ''; // Priority #2: Empty - No participant slots are filled if (filledSlots.length === 0) { priorityType = 'empty'; priorityLabel = 'Empty'; } // Priority #1: Waiting - Participant slots not fully filled, and no slots with "in progress" status else if (filledSlots.length < slots.length && !slotStatuses.some(s => s.filled && s.status === 'inProgress')) { priorityType = 'waiting'; priorityLabel = 'Waiting'; } // Priority #3: Nobody queueing - One or more slots filled, all are completed or in progress, rest are empty else if (filledSlots.length > 0 && filledSlots.every(slot => ['completed', 'inProgress'].includes(getSlotStatus(slot))) && slots.every(slot => isSlotFilled(slot) || getSlotStatus(slot) === 'empty')) { priorityType = 'nobodyQueueing'; priorityLabel = 'Nobody Queueing'; } // Priority #4: All others else { priorityType = 'others'; priorityLabel = ''; } // Apply highlights based on priority if (priorityType && priorityType !== 'others') { card.style.backgroundColor = PRIORITY_COLORS[priorityType]; card.style.boxShadow = `0 0 8px ${PRIORITY_COLORS[priorityType]}`; // Create priority badge if it's not "others" const priorityBadge = document.createElement('div'); priorityBadge.className = 'priority-badge'; priorityBadge.style.position = 'absolute'; priorityBadge.style.top = '0'; priorityBadge.style.left = '0'; priorityBadge.style.backgroundColor = PRIORITY_COLORS[priorityType].replace('0.4', '0.8'); priorityBadge.style.color = '#fff'; priorityBadge.style.padding = '3px 8px'; priorityBadge.style.borderRadius = '0 0 8px 0'; priorityBadge.style.fontSize = '12px'; priorityBadge.style.fontWeight = 'bold'; priorityBadge.style.zIndex = '10'; priorityBadge.style.textShadow = '1px 1px 1px rgba(0,0,0,0.5)'; // Set badge text based on priority priorityBadge.textContent = `#${priorityType === 'waiting' ? '1' : priorityType === 'empty' ? '2' : '3'} ${priorityLabel}`; // Add the badge to the card card.querySelector('.contentLayer___IYFdz').appendChild(priorityBadge); } // Count priorities for stats if (priorityType) { priorityCounts[priorityType]++; } } } }); // Process join buttons based on minimum success chance const joinButtonStats = processJoinButtons(minSuccess); return { priorityCounts, joinButtonStats }; } function updateFilters() { const showCheckbox = document.getElementById('crimeFilterCheckbox'); const levelInput = document.getElementById('minCrimeLevelInput'); const highlightPriorityCheckbox = document.getElementById('highlightPriorityCheckbox'); const minSuccessInput = document.getElementById('minSuccessInput'); const shouldShow = showCheckbox.checked; const minLevel = parseInt(levelInput.value, 10) || 1; const highlightPriority = highlightPriorityCheckbox.checked; const minSuccess = parseInt(minSuccessInput.value, 10) || DEFAULT_SETTINGS.minSuccess; // Save filter settings saveSettings({ show: shouldShow, level: minLevel, highlightPriority: highlightPriority, minSuccess: minSuccess }); // Process crime cards and get statistics const processResult = processCrimeCards(highlightPriority, minSuccess); const priorityCounts = processResult.priorityCounts; const joinButtonStats = processResult.joinButtonStats; // Update status text const statusEl = document.getElementById('filterStatus'); let visibleCount = 0; let totalCount = 0; let filledCount = 0; let openCount = 0; document.querySelectorAll('.wrapper___U2Ap7').forEach(card => { const levelEl = card.querySelector('.level___sBl49'); totalCount++; let shouldDisplay = true; if (levelEl) { const text = levelEl.textContent.trim(); const parts = text.split('/'); const level = parseInt(parts[0], 10) || 0; // Apply level filter if checkbox is checked if (shouldShow && level < minLevel) { shouldDisplay = false; } } // Count filled and open slots for visible cards if (shouldDisplay) { visibleCount++; // Get filled slots count for this card const participantsContainer = card.querySelector('.wrapper___g3mPt'); if (participantsContainer) { const allSlots = participantsContainer.querySelectorAll('.wrapper___Lpz_D').length; const filledSlots = participantsContainer.querySelectorAll('.validSlot___n6ueL').length; filledCount += filledSlots; openCount += (allSlots - filledSlots); } } // Apply visibility card.style.display = shouldDisplay ? '' : 'none'; }); // Update status with priority counts and join button stats let priorityText = ''; if (highlightPriority) { priorityText = ` | P1: ${priorityCounts.waiting} | P2: ${priorityCounts.empty} | P3: ${priorityCounts.nobodyQueueing}`; } let successText = ''; if (joinButtonStats.totalButtons > 0) { successText = ` | Blocked: ${joinButtonStats.disabledCount}/${joinButtonStats.totalButtons} join buttons`; } statusEl.textContent = `Showing ${visibleCount} of ${totalCount} crimes | ${filledCount} filled slots | ${openCount} open slots${priorityText}${successText}`; } window.addEventListener('load', function() { const crimesContainer = document.getElementById('faction-crimes'); if (!crimesContainer) return; // Create custom CSS for the badges const style = document.createElement('style'); style.textContent = ` .participant-badge, .priority-badge { transition: all 0.2s ease; } .wrapper___U2Ap7:hover .participant-badge { background-color: rgba(40, 167, 69, 0.8) !important; } .wrapper___U2Ap7:hover .priority-badge { filter: brightness(1.2); } /* Success chance tooltip */ .join-button-tooltip { position: absolute; background-color: rgba(0, 0, 0, 0.9); color: white; padding: 5px 8px; border-radius: 4px; font-size: 12px; z-index: 10001; max-width: 250px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); pointer-events: none; transition: opacity 0.15s; white-space: nowrap; opacity: 0; } /* Legend styles */ .highlight-legend { margin-top: 8px; display: grid; grid-template-columns: 12px auto; grid-gap: 5px 8px; align-items: center; font-size: 12px; } .legend-color { width: 12px; height: 12px; border-radius: 2px; } `; document.head.appendChild(style); // Load saved settings const settings = loadSettings(); // Create filter UI container const filterDiv = document.createElement('div'); Object.assign(filterDiv.style, { position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0, 0, 0, 0.8)', color: '#fff', padding: '12px', zIndex: '10000', borderRadius: '8px', fontFamily: 'Arial, sans-serif', fontSize: '14px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', display: 'flex', flexDirection: 'column', gap: '8px', transition: 'opacity 0.1s ease', cursor: 'default' }); // Create header with title and collapse button const headerDiv = document.createElement('div'); headerDiv.style.display = 'flex'; headerDiv.style.justifyContent = 'space-between'; headerDiv.style.alignItems = 'center'; headerDiv.style.marginBottom = '8px'; headerDiv.style.cursor = 'move'; headerDiv.style.userSelect = 'none'; // Prevent text selection while dragging const titleSpan = document.createElement('span'); titleSpan.textContent = 'Crime Filter'; titleSpan.style.fontWeight = 'bold'; const collapseButton = document.createElement('button'); collapseButton.textContent = settings.isCollapsed ? '▼' : '▲'; collapseButton.style.background = 'none'; collapseButton.style.border = 'none'; collapseButton.style.color = '#fff'; collapseButton.style.cursor = 'pointer'; collapseButton.style.padding = '0 5px'; // Create content container const contentDiv = document.createElement('div'); contentDiv.style.display = settings.isCollapsed ? 'none' : 'block'; // Create controls container const controlsDiv = document.createElement('div'); controlsDiv.style.display = 'flex'; controlsDiv.style.flexDirection = 'column'; controlsDiv.style.gap = '10px'; // Create first row (level filter) const filterRow = document.createElement('div'); filterRow.style.display = 'flex'; filterRow.style.alignItems = 'center'; filterRow.style.gap = '10px'; // Create checkbox group const checkboxGroup = document.createElement('div'); checkboxGroup.style.display = 'flex'; checkboxGroup.style.alignItems = 'center'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'crimeFilterCheckbox'; checkbox.checked = settings.show; checkbox.style.marginRight = '5px'; const checkboxLabel = document.createElement('label'); checkboxLabel.htmlFor = 'crimeFilterCheckbox'; checkboxLabel.textContent = 'Filter by Level'; checkboxLabel.title = 'When checked, filters crime cards by level. When unchecked, shows all cards.'; checkboxLabel.style.cursor = 'pointer'; // Create level input group const levelGroup = document.createElement('div'); levelGroup.style.display = 'flex'; levelGroup.style.alignItems = 'center'; const levelLabel = document.createElement('label'); levelLabel.htmlFor = 'minCrimeLevelInput'; levelLabel.textContent = 'Min Level:'; const levelInput = document.createElement('input'); levelInput.type = 'number'; levelInput.id = 'minCrimeLevelInput'; levelInput.min = '1'; levelInput.max = '10'; levelInput.value = settings.level; levelInput.style.width = '45px'; levelInput.style.marginLeft = '5px'; levelInput.style.padding = '2px 4px'; // Create second row (highlight checkbox and success chance input) const highlightRow = document.createElement('div'); highlightRow.style.display = 'flex'; highlightRow.style.alignItems = 'center'; highlightRow.style.justifyContent = 'space-between'; // Highlight checkbox group const highlightCheckboxGroup = document.createElement('div'); highlightCheckboxGroup.style.display = 'flex'; highlightCheckboxGroup.style.alignItems = 'center'; const highlightCheckbox = document.createElement('input'); highlightCheckbox.type = 'checkbox'; highlightCheckbox.id = 'highlightPriorityCheckbox'; highlightCheckbox.checked = settings.highlightPriority; highlightCheckbox.style.marginRight = '5px'; const highlightLabel = document.createElement('label'); highlightLabel.htmlFor = 'highlightPriorityCheckbox'; highlightLabel.textContent = 'Highlight Priority'; highlightLabel.title = 'Highlights crimes based on priority status'; highlightLabel.style.cursor = 'pointer'; // Min success chance group const successGroup = document.createElement('div'); successGroup.style.display = 'flex'; successGroup.style.alignItems = 'center'; const successLabel = document.createElement('label'); successLabel.htmlFor = 'minSuccessInput'; successLabel.textContent = 'Min Success:'; const successInput = document.createElement('input'); successInput.type = 'number'; successInput.id = 'minSuccessInput'; successInput.min = '0'; successInput.max = '100'; successInput.value = settings.minSuccess; successInput.style.width = '45px'; successInput.style.marginLeft = '5px'; successInput.style.padding = '2px 4px'; // Create legend for highlight colors const legendDiv = document.createElement('div'); legendDiv.className = 'highlight-legend'; legendDiv.style.display = settings.highlightPriority ? 'grid' : 'none'; // Priority #1 legend const p1ColorDiv = document.createElement('div'); p1ColorDiv.className = 'legend-color'; p1ColorDiv.style.backgroundColor = PRIORITY_COLORS.waiting; const p1TextDiv = document.createElement('div'); p1TextDiv.textContent = '#1 Waiting'; // Priority #2 legend const p2ColorDiv = document.createElement('div'); p2ColorDiv.className = 'legend-color'; p2ColorDiv.style.backgroundColor = PRIORITY_COLORS.empty; const p2TextDiv = document.createElement('div'); p2TextDiv.textContent = '#2 Empty'; // Priority #3 legend const p3ColorDiv = document.createElement('div'); p3ColorDiv.className = 'legend-color'; p3ColorDiv.style.backgroundColor = PRIORITY_COLORS.nobodyQueueing; const p3TextDiv = document.createElement('div'); p3TextDiv.textContent = '#3 Nobody Queueing'; // Add legend items legendDiv.appendChild(p1ColorDiv); legendDiv.appendChild(p1TextDiv); legendDiv.appendChild(p2ColorDiv); legendDiv.appendChild(p2TextDiv); legendDiv.appendChild(p3ColorDiv); legendDiv.appendChild(p3TextDiv); // Create status text const statusEl = document.createElement('div'); statusEl.id = 'filterStatus'; statusEl.style.fontSize = '12px'; statusEl.style.color = '#aaa'; statusEl.style.marginTop = '4px'; // Assemble the UI headerDiv.appendChild(titleSpan); headerDiv.appendChild(collapseButton); checkboxGroup.appendChild(checkbox); checkboxGroup.appendChild(checkboxLabel); levelGroup.appendChild(levelLabel); levelGroup.appendChild(levelInput); filterRow.appendChild(checkboxGroup); filterRow.appendChild(levelGroup); highlightCheckboxGroup.appendChild(highlightCheckbox); highlightCheckboxGroup.appendChild(highlightLabel); successGroup.appendChild(successLabel); successGroup.appendChild(successInput); highlightRow.appendChild(highlightCheckboxGroup); highlightRow.appendChild(successGroup); controlsDiv.appendChild(filterRow); controlsDiv.appendChild(highlightRow); controlsDiv.appendChild(legendDiv); contentDiv.appendChild(controlsDiv); contentDiv.appendChild(statusEl); filterDiv.appendChild(headerDiv); filterDiv.appendChild(contentDiv); document.body.appendChild(filterDiv); // Set initial position from saved settings filterDiv.style.transform = `translate(${settings.positionX}px, ${settings.positionY}px)`; // Collapse button functionality collapseButton.addEventListener('click', (e) => { e.stopPropagation(); settings.isCollapsed = !settings.isCollapsed; contentDiv.style.display = settings.isCollapsed ? 'none' : 'block'; collapseButton.textContent = settings.isCollapsed ? '▼' : '▲'; saveSettings({ isCollapsed: settings.isCollapsed }); }); // Highlight checkbox functionality highlightCheckbox.addEventListener('change', function() { legendDiv.style.display = this.checked ? 'grid' : 'none'; updateFilters(); }); // Add event listeners for filters checkbox.addEventListener('change', updateFilters); levelInput.addEventListener('input', updateFilters); levelInput.addEventListener('change', updateFilters); successInput.addEventListener('input', updateFilters); successInput.addEventListener('change', updateFilters); // Improved drag and drop implementation let isDragging = false; let xOffset = settings.positionX; let yOffset = settings.positionY; let startX, startY; function dragStart(e) { // Only start dragging if clicking on the header or title if (e.target === headerDiv || e.target === titleSpan) { e.preventDefault(); isDragging = true; // Use clientX/Y for mouse position startX = e.clientX; startY = e.clientY; // Add visual feedback for dragging filterDiv.style.opacity = '0.8'; filterDiv.style.transition = 'opacity 0.1s ease'; document.body.style.cursor = 'move'; // Disable text selection document.body.style.userSelect = 'none'; } } function drag(e) { if (!isDragging) return; e.preventDefault(); // Calculate how far we've moved const dx = e.clientX - startX; const dy = e.clientY - startY; // Update position based on movement xOffset += dx; yOffset += dy; // Update position directly with transform filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`; // Update starting point for next movement startX = e.clientX; startY = e.clientY; } function dragEnd(e) { if (!isDragging) return; // Clean up isDragging = false; filterDiv.style.opacity = '1'; document.body.style.cursor = 'default'; document.body.style.userSelect = ''; // Check bounds and adjust if needed const bounds = keepInBounds(filterDiv); xOffset = bounds.x; yOffset = bounds.y; // Apply final position filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`; // Save position saveSettings({ positionX: xOffset, positionY: yOffset }); } // Add event listeners for dragging headerDiv.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); // Also end drag if mouse leaves the window document.addEventListener('mouseleave', dragEnd); // Handle window resize to keep the box in bounds window.addEventListener('resize', () => { const bounds = keepInBounds(filterDiv); xOffset = bounds.x; yOffset = bounds.y; filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`; saveSettings({ positionX: xOffset, positionY: yOffset }); }); // Initial filter application updateFilters(); // Set up observer for dynamic content const observer = new MutationObserver(() => { // Delay the update slightly to ensure DOM is fully updated setTimeout(updateFilters, 100); }); observer.observe(crimesContainer, { childList: true, subtree: true }); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址