您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extension to 4chanX that adds support for delayed posts. Useful for delaying your posts when you want to keep a slow thread bumped while idle.
// ==UserScript== // @name FuturePost // @version 1.1 // @include http://boards.4chan.org/* // @include https://boards.4chan.org/* // @include http://sys.4chan.org/* // @include https://sys.4chan.org/* // @include http://www.4chan.org/* // @include https://www.4chan.org/* // @include http://boards.4channel.org/* // @include https://boards.4channel.org/* // @include http://sys.4channel.org/* // @include https://sys.4channel.org/* // @include http://www.4channel.org/* // @include https://www.4channel.org/* // @license MIT // @require https://code.jquery.com/jquery-3.5.1.min.js // @namespace PyonScripts // @description Extension to 4chanX that adds support for delayed posts. Useful for delaying your posts when you want to keep a slow thread bumped while idle. // ==/UserScript== // A separate queue for each page let pageQueues = { '2': [], '3': [], '4': [], '5': [], '6': [], '7': [], '8': [], '9': [], '10': [], '11': [] }; function delay(time) { return new Promise(resolve => setTimeout(resolve, time)); } // The main loop that will be responsible for posting the enqueued posts setInterval(function() { const pageCountElement = $('#page-count'); // if post-count has the class warning, don't post anything if (document.querySelector("#post-count").classList.contains('warning')) { return; } const currentPage = Number(pageCountElement.text()); for (let i = 3; i <= currentPage; i++) { if (pageQueues[i.toString()] && pageQueues[i.toString()].length > 0) { const post = pageQueues[i.toString()].shift(); document.querySelector('#shortcut-qr a').click() delay(1000).then(() => { if(post.file) { var detail = {file: post.file, name: post.fileName}; if (typeof cloneInto === 'function') { detail = cloneInto(detail, document.defaultView); } var event = new CustomEvent('QRSetFile', {bubbles: true, detail: detail}); document.dispatchEvent(event); } document.querySelector('#qr textarea').value = post.message; }); delay(2000).then(() => { document.querySelector("#file-n-submit input[type='submit']").click(); // Attempt to remove the preview from the thread const fakePost = document.getElementById(`fp${post.mockNumber}`); if (fakePost) { fakePost.remove(); } }); console.log('Remaining queue', pageQueues); break; } } }, 60000); function removeFuturePost(postNumber) { // Iterate over each page queue in the pageQueues object for (const queueName in pageQueues) { const queue = pageQueues[queueName]; // Find the index of the object with a matching postNumber in the queue const index = queue.findIndex(obj => obj.mockNumber === postNumber); // If the object is found, remove it from the queue if (index !== -1) { queue.splice(index, 1); removePostPreview(postNumber); break; } } console.log('Remaining queue', pageQueues); } function removePostPreview(postNumber) { // Attempt to remove the preview from the thread const fakePost = document.getElementById(`fp${postNumber}`); if (fakePost) { fakePost.remove(); } } function initExtension() { var qrElement = document.querySelector('#qr.dialog'); var futurePostEl = document.querySelector('#future-post'); // Stop init if it can't find the quickly reply dialog, or if this extension // has already been initialized if (!qrElement || futurePostEl) { return; } const originalButton = qrElement.querySelector('input[type="submit"]'); if (!originalButton) { return; } // Create and style your fake button var fakeButton = document.createElement('input'); fakeButton.type = 'submit'; fakeButton.value = "Submit"; fakeButton.id = 'future-post'; // Submit handler for the delayed post button fakeButton.addEventListener('click', (e) => { e.preventDefault(); // Prevents the default action e.stopPropagation(); // Prevents the event from propagating up the DOM tree, preventing any parent handlers from being notified of the event // Your custom submit behavior here const textArea = $('#qr textarea'); const fileInput = document.querySelector('#qr input[type="file"]'); // replace with your actual file input ID const pageNum = $('#fp-page-num').val(); let fileBlob; let fileName; document.addEventListener('QRFile', function(e) { if(e.detail) { fileName = e.detail.newName; fileBlob = new Blob([e.detail], { type: e.detail.type }); } }, false); var event = new CustomEvent('QRGetFile', {bubbles: true, detail: null}); document.dispatchEvent(event); // Generate a random post number const randomNumber = Math.floor(Math.random() * 1000000); let post = { mockNumber: randomNumber, message: textArea.val(), file: fileBlob, // this will be null if no file was selected fileName: fileName // this will be null if no file was selected }; pageQueues[pageNum].push(post); // Try to reset the form, doings lots of extra stuff i prob don't have to $('#fp-checkbox').prop('checked', false).trigger('change'); fileInput.value = ''; textArea.val(''); const closeAnchor = document.querySelector('#qr .close'); closeAnchor.click(); console.log(pageQueues); // Add a placeholder delayed post to the DOM const threadDiv = document.querySelector('.thread'); // Create a new postContainer element const postContainer = document.createElement('div'); postContainer.classList.add('replyContainer'); postContainer.id = `fp${post.mockNumber}` let fileInfo = ''; if(fileName) { const fileExtension = fileName.split('.').pop().toLowerCase(); let imgThumb = ''; if (['jpg', 'gif', 'png', 'jpeg'].includes(fileExtension)) { // Create a File blob with the fileName and set it as the img src const imgSrc = URL.createObjectURL(fileBlob); const fileSizeInBytes = fileBlob.size; const fileSizeInKB = Math.round(fileSizeInBytes / 1024); imgThumb = `<a class="fileThumb" href="javascript:void(0);" target="_blank"> <img src="${imgSrc}" alt="${fileSizeInKB} KB" style="width: 125px;" loading="lazy"> </a>`; } fileInfo = `<div class="file"> <div class="fileText"> <span class="file-info"> <a href="javascript:void(0);" target="_blank">${fileName}</a> </span> </div> ${imgThumb} </div>` } // Set the innerHTML of the postContainer element with the provided HTML, replacing the message content postContainer.innerHTML = ` <div class="post reply" style="border: 1px dotted yellow;"> <div class="postInfoM mobile"> <span class="nameBlock"><span class="name">Anonymous</span><br /></span> </div> <div class="postInfo desktop"> <input type="checkbox" name="51464347" value="delete" /> <span class="nameBlock"><span class="name">Anonymous</span> </span> <span class="dateTime" title="This post will not be submitted until the page number that is shown" > Page ${pageNum} </span> <span class="postNum desktop"><a href="#fp${post.mockNumber}" title="Link to this post">No.</a><a href="javascript:void(0);" title="You can't reply to a future post">Pending</a></span> <a class="menu-button delete-future-post" href="javascript:void(0);"><i class="fa fa-times"></i></a> </div> ${fileInfo} <blockquote class="postMessage" style="min-width: 300px"> ${post.message.replace(/\n/g, '<br>')} <br /> <br /> <strong style="color: red;">(Only visible to you)</strong> </blockquote> </div> `; // Append the new postContainer element to the thread div threadDiv.appendChild(postContainer); postContainer.querySelector('.delete-future-post').addEventListener('click', function(e) { e.preventDefault(); const confirmation = confirm('Remove this future post?'); if (confirmation) { removeFuturePost(post.mockNumber) } }); }); // Append checkbox to qrElement var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; const futurePostHtml = `<div> Delay Post? <input type="checkbox" id="fp-checkbox" /> Page: <select id="fp-page-num" required disabled> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> <option value="6" selected>6</option> <option value="7">7</option> <option value="8">8</option> <option value="9">9</option> <option value="10">10</option> <option value="11">11</option> </select> </div> `; $(qrElement).append(futurePostHtml); $(document).on('change', '#fp-checkbox', function() { if($(this).is(":checked")) { $('#fp-page-num').prop('disabled', false); // Check if fakeButton already exists in the DOM if (document.querySelector('#future-post')) { // If it does, simply show it fakeButton.style.display = "inline"; } else { // Otherwise, add it to the DOM $(originalButton).after(fakeButton); } originalButton.style.display = 'none'; } else { $('#fp-page-num').prop('disabled', true); // Replace the fake button with the original one fakeButton.style.display = "none"; originalButton.style.display = "inline"; } }); startIdleStats(); } function startIdleStats() { let lastUpdateTime = Date.now(); let changeDurations = []; // keep track of all durations between changes // Create a callback function to be executed whenever a mutation is observed const callback = function(mutationsList, observer) { for(let mutation of mutationsList) { if (mutation.type === 'childList') { let currentTime = Date.now(); let duration = currentTime - lastUpdateTime; lastUpdateTime = currentTime; changeDurations.push(duration); dispatchIdleMetricsUpdate(); // dispatch event when metrics change } } }; // Create an observer instance linked to the callback function let observer = new MutationObserver(callback); // Start observing the 'page-count' node for configured mutations let targetNode = document.getElementById('page-count'); observer.observe(targetNode, { childList: true, subtree: true }); // Calculate the weighted average change duration function calculateWeightedAverageDuration() { let weightedSum = 0; let totalWeight = 0; for(let i = 0; i < changeDurations.length; i++) { let weight = i + 1; // more recent changes have a larger weight weightedSum += changeDurations[i] * weight; totalWeight += weight; } let average = weightedSum / totalWeight; return average / 60000; // convert from ms to minutes } // Calculate the approximate time before the thread hits page 11 function calculateIdleTimeBeforePageEleven() { let weightedAvg = calculateWeightedAverageDuration(); let totalBumpTime = 0; for (let page in pageQueues) { totalBumpTime += pageQueues[page].length * page * weightedAvg; } let totalPageChanges = 11; let approximateTime = totalPageChanges * weightedAvg + totalBumpTime; return approximateTime; } // Dispatch an event with the updated idle metrics function dispatchIdleMetricsUpdate() { let event = new CustomEvent('idleMetricsUpdate', { detail: { idleTimeBeforePageEleven: calculateIdleTimeBeforePageEleven(), weightedAvgDuration: calculateWeightedAverageDuration(), } }); window.dispatchEvent(event); } window.addEventListener('idleMetricsUpdate', function(e) { let idleTimeBeforePageEleven = e.detail.idleTimeBeforePageEleven; let hours = Math.floor(idleTimeBeforePageEleven / 60); let minutes = Math.floor(idleTimeBeforePageEleven % 60); let weightedAvgDuration = e.detail.weightedAvgDuration.toFixed(2); // rounded to 2 decimal places // Display the report in the console with a label for easy filtering console.log( `%cIdle Metrics Report:\n\n` + `Idle time before page 11: ${hours} hour(s) and ${minutes} minute(s)\n` + `Weighted average duration: ${weightedAvgDuration} minute(s)`, 'color: green; font-weight: bold;' ); }); } // Initialize the extension document.addEventListener('QRDialogCreation', function(e) { initExtension(); }, false);