您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
KiwiSDR Scheduled Recorder, timed recorder
// ==UserScript== // @name KiwiSDR Scheduled Recorder // @namespace http://tampermonkey.net/ // @version 0.6 // @description KiwiSDR Scheduled Recorder, timed recorder // @author JerryXu09 // @license MIT // @match http://*.proxy.kiwisdr.com/* // @grant none // ==/UserScript== (function() { 'use strict'; // Language support const LANG = { zh: { title: 'KiwiSDR Scheduled Recorder', setRecord: '显示/隐藏菜单', startTime: '开始时间', endTime: '结束时间', saveWF: '保存频谱图 (仅结束前5分钟)', confirm: '确认', cancel: '取消', recording: '录制中...', scheduled: '已计划录制', invalidTime: '请输入有效的时间格式!', startAfterNow: '开始时间必须在当前时间之后!', endAfterStart: '结束时间必须在开始时间之后!', alreadyRecording: '检测到已在录音,将在指定时间停止', recordingStarted: '录音已开始', recordingStopped: '录音已停止', wfSaved: '频谱图已保存', wfButtonNotFound: '保存频谱图按钮未找到', recordingInterrupted: '检测到录音被中断,取消定时任务', timeFormat: 'YYYY-MM-DD HH:MM:SS' }, en: { title: 'KiwiSDR Scheduled Recorder', setRecord: 'Show/Hide Menu', startTime: 'Start Time', endTime: 'End Time', saveWF: 'Save Waterfall (Last 5 min only)', confirm: 'Confirm', cancel: 'Cancel', recording: 'Recording...', scheduled: 'Scheduled', invalidTime: 'Please enter valid time format!', startAfterNow: 'Start time must be after current time!', endAfterStart: 'End time must be after start time!', alreadyRecording: 'Recording detected, will stop at specified time', recordingStarted: 'Recording started', recordingStopped: 'Recording stopped', wfSaved: 'Waterfall saved', wfButtonNotFound: 'Save waterfall button not found', recordingInterrupted: 'Recording interrupted, canceling scheduled task', timeFormat: 'YYYY-MM-DD HH:MM:SS' } }; // Detect language from browser const currentLang = navigator.language.startsWith('zh') ? 'zh' : 'en'; const t = LANG[currentLang]; // State management let recordingState = { isRecording: false, startTimer: null, stopTimer: null, statusMonitor: null, isScheduled: false }; // Get recording button const getRecButton = () => document.querySelector('.id-rec1'); // Get save waterfall button const getSaveWFButton = () => document.querySelector('.id-btn-grp-56'); // Check if currently recording by looking for spinning animation const isCurrentlyRecording = () => { const button = getRecButton(); return button && button.classList.contains('fa-spin'); }; // Click button function const clickButton = (button) => { if (button) { button.click(); return true; } return false; }; // Format date time const formatDateTime = (date) => { const pad = (n) => n < 10 ? '0' + n : n; return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); }; // Parse time input (supports seconds) const parseTimeInput = (timeStr) => { // Try to parse the input time string const cleanStr = timeStr.replace(/-/g, '/'); const date = new Date(cleanStr); return isNaN(date) ? null : date; }; // Create draggable floating panel const createFloatingPanel = () => { // Main container const container = document.createElement('div'); container.id = 'kiwi-auto-record-panel'; container.style.cssText = ` position: fixed; top: 20px; right: 20px; width: 280px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: white; overflow: hidden; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); `; // Header (draggable) const header = document.createElement('div'); header.style.cssText = ` background: rgba(255,255,255,0.1); padding: 12px 16px; cursor: move; font-weight: 600; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.1); `; header.textContent = t.title; // Content area const content = document.createElement('div'); content.style.cssText = ` padding: 16px; display: none; `; // Toggle button const toggleBtn = document.createElement('button'); toggleBtn.style.cssText = ` width: 100%; padding: 10px; background: rgba(255,255,255,0.2); border: none; color: white; cursor: pointer; font-weight: 500; transition: all 0.3s ease; `; toggleBtn.textContent = t.setRecord; toggleBtn.onmouseover = () => toggleBtn.style.background = 'rgba(255,255,255,0.3)'; toggleBtn.onmouseout = () => toggleBtn.style.background = 'rgba(255,255,255,0.2)'; // Set default times const now = new Date(); const startDefault = new Date(now.getTime() + 2 * 60 * 1000); // 2 minutes later const stopDefault = new Date(now.getTime() + 7 * 60 * 1000); // 7 minutes later // Form elements content.innerHTML = ` <div style="margin-bottom: 12px;"> <label style="display: block; margin-bottom: 6px; font-size: 13px; opacity: 0.9;">${t.startTime}:</label> <input id="startTimeInput" type="text" value="${formatDateTime(startDefault)}" style="width: 100%; padding: 8px; border: none; border-radius: 6px; background: rgba(255,255,255,0.9); color: #333; font-size: 12px;"> </div> <div style="margin-bottom: 12px;"> <label style="display: block; margin-bottom: 6px; font-size: 13px; opacity: 0.9;">${t.endTime}:</label> <input id="endTimeInput" type="text" value="${formatDateTime(stopDefault)}" style="width: 100%; padding: 8px; border: none; border-radius: 6px; background: rgba(255,255,255,0.9); color: #333; font-size: 12px;"> </div> <div style="margin-bottom: 16px;"> <label style="display: flex; align-items: center; font-size: 13px; cursor: pointer;"> <input id="saveWFCheckbox" type="checkbox" style="margin-right: 8px;"> ${t.saveWF} </label> </div> <div style="display: flex; gap: 8px;"> <button id="confirmBtn" style="flex: 1; padding: 10px; background: #4CAF50; border: none; border-radius: 6px; color: white; cursor: pointer; font-weight: 500;"> ${t.confirm} </button> <button id="cancelBtn" style="flex: 1; padding: 10px; background: #f44336; border: none; border-radius: 6px; color: white; cursor: pointer; font-weight: 500;"> ${t.cancel} </button> </div> <div id="statusDisplay" style="margin-top: 12px; font-size: 12px; text-align: center; opacity: 0.8;"></div> `; container.appendChild(header); container.appendChild(toggleBtn); container.appendChild(content); document.body.appendChild(container); // Make draggable let isDragging = false; let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0; header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (e.target === header) { isDragging = true; } } function drag(e) { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; container.style.transform = `translate(${currentX}px, ${currentY}px)`; } } function dragEnd() { isDragging = false; } // Toggle content visibility toggleBtn.addEventListener('click', () => { const isVisible = content.style.display !== 'none'; content.style.display = isVisible ? 'none' : 'block'; toggleBtn.textContent = recordingState.isScheduled ? t.scheduled : t.setRecord; }); return { container, content, toggleBtn, statusDisplay: content.querySelector('#statusDisplay') }; }; // Monitor recording status const startStatusMonitor = () => { recordingState.statusMonitor = setInterval(() => { const wasRecording = recordingState.isRecording; recordingState.isRecording = isCurrentlyRecording(); // Detect recording interruption if (wasRecording && !recordingState.isRecording && recordingState.isScheduled) { console.log(t.recordingInterrupted); clearScheduledTasks(); updateStatus(t.recordingInterrupted); } }, 1000); }; // Clear all scheduled tasks const clearScheduledTasks = () => { if (recordingState.startTimer) { clearTimeout(recordingState.startTimer); recordingState.startTimer = null; } if (recordingState.stopTimer) { clearTimeout(recordingState.stopTimer); recordingState.stopTimer = null; } if (recordingState.statusMonitor) { clearInterval(recordingState.statusMonitor); recordingState.statusMonitor = null; } recordingState.isScheduled = false; ui.toggleBtn.textContent = t.setRecord; }; // Update status display const updateStatus = (message) => { ui.statusDisplay.textContent = message; console.log(message); }; // Main scheduling function const scheduleRecording = (startTime, endTime, saveWF) => { const now = new Date(); const startDelay = startTime - now; const stopDelay = endTime - now; const currentlyRecording = isCurrentlyRecording(); clearScheduledTasks(); recordingState.isScheduled = true; ui.toggleBtn.textContent = t.scheduled; if (currentlyRecording) { updateStatus(t.alreadyRecording); // Only schedule stop recordingState.stopTimer = setTimeout(() => { const button = getRecButton(); if (clickButton(button)) { updateStatus(t.recordingStopped); recordingState.isRecording = false; if (saveWF) { setTimeout(() => { const wfButton = getSaveWFButton(); if (clickButton(wfButton)) { updateStatus(t.wfSaved); } else { updateStatus(t.wfButtonNotFound); } }, 1000); } } clearScheduledTasks(); }, stopDelay); } else { // Schedule both start and stop recordingState.startTimer = setTimeout(() => { const button = getRecButton(); if (clickButton(button)) { updateStatus(t.recordingStarted); recordingState.isRecording = true; recordingState.stopTimer = setTimeout(() => { if (clickButton(button)) { updateStatus(t.recordingStopped); recordingState.isRecording = false; if (saveWF) { setTimeout(() => { const wfButton = getSaveWFButton(); if (clickButton(wfButton)) { updateStatus(t.wfSaved); } else { updateStatus(t.wfButtonNotFound); } }, 1000); } } clearScheduledTasks(); }, stopDelay - startDelay); } }, startDelay); } startStatusMonitor(); }; // Initialize UI const ui = createFloatingPanel(); // Event handlers ui.content.querySelector('#confirmBtn').addEventListener('click', () => { const startTimeStr = document.getElementById('startTimeInput').value; const endTimeStr = document.getElementById('endTimeInput').value; const saveWF = document.getElementById('saveWFCheckbox').checked; const startTime = parseTimeInput(startTimeStr); const endTime = parseTimeInput(endTimeStr); const now = new Date(); // Validation if (!startTime || !endTime) { alert(t.invalidTime); return; } if (startTime <= now) { alert(t.startAfterNow); return; } if (endTime <= startTime) { alert(t.endAfterStart); return; } scheduleRecording(startTime, endTime, saveWF); ui.content.style.display = 'none'; }); ui.content.querySelector('#cancelBtn').addEventListener('click', () => { ui.content.style.display = 'none'; }); // Initialize recording state recordingState.isRecording = isCurrentlyRecording(); console.log(`${t.title} loaded successfully`); })();