FuturePost

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.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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);