Posted on

Ciągle ulepszam moją skryptozakładkę do Facebooka, mam już jej trzecią wersję, i pomysły na kolejne piętnaście.

To czego nie mam, to koncepcji sensownego jej wersjonowania - i w ogóle całego kodu na stronie. Teoretycznie mogę wrzucać każdą wersję jako osobny plik, bo każdy wpis i tak żyje w swoim własnym folderze, ale jeszcze nie pogodziłem się z tym pomysłem, bo bardzo lubię Gita. Ale stawianie repo kłóci się z moim podejściem upraszczania do granic możliwości. No i czymże jest Git, jeśli po prostu nie kolejnymi kopiami tego samego zaczarowanymi w skomplikowane komendy? Na razie wrzucam jako osobny plik.

Dodałem "tryb skupienia" dla pojedynczego posta. Żeby dobrze działało, to post trzeba otworzyć w osobnej zakładce, bo skrypt dosłownie wyszukuje nagłówka postu, a następnie wychodzi trzynaście divów do góry i robi czary za pomocą stylu CSS visibility. Oraz przyciski do rozwijania komentarzy są od teraz klikane dopiero wtedy, gdy pojawią się na ekranie. I są zaznaczane na czerwono, żeby było widoczne, co się klika.

javascript: (function () {
  const PANEL_ID = "control-panel";
  const FOCUS_LEVEL = 13;
  const FOCUS_MODE_STYLES = `
    <style id="focus-mode-styles">
      .focus-hidden { visibility: hidden !important; }
      .focus-visible { visibility: visible !important; }
    </style>
  `;
  const COMMENT_BUTTON_PATTERNS = [
    /Wyświetl wszystkie.*odpowiedzi/,
    /Wyświetl więcej/,
    /Pokaż więcej odpowiedzi/,
    /Wyświetl 1 odpowiedź/,
    /odpowiedzia(ł|ła|\(a\))/,
  ];

  /* if panel already exists, do nothing */
  if (document.getElementById(PANEL_ID)) return;

  let domWatcher = null;
  let visibilityObserver = null;
  let dragOffset = null;
  let clickedButtonsCount = 0;
  let focusEnabled = false;

  /* add focus mode CSS styles */
  document.head.insertAdjacentHTML("beforeend", FOCUS_MODE_STYLES);

  /* build and display control panel */
  const panel = createPanel();
  const title = panel.querySelector("#panel-title");
  const commentButton = panel.querySelector("#comment-button");
  const counterDisplay = panel.querySelector("#counter-display");
  const focusModeButton = panel.querySelector("#focus-mode-button");
  const closeButton = panel.querySelector("#close-button");
  document.body.appendChild(panel);

  /* event handlers */
  title.onmousedown = (e) => startDragging(e);
  document.onmousemove = (e) => dragPanel(e);
  document.onmouseup = () => stopDragging();
  commentButton.onclick = () => (visibilityObserver ? stopWatching() : startWatching());
  focusModeButton.onclick = () => (focusEnabled ? deactivateFocusMode() : activateFocusMode());
  closeButton.onclick = closePanel;

  /* === MAIN FUNCTIONS === */

  function startWatching() {
    updateButton(commentButton, "Wyłącz rozwijanie komentarzy", "crimson");
    visibilityObserver = new IntersectionObserver((entries) => clickIntersectingEntries(entries));
    observeNode(document.body, visibilityObserver);
    domWatcher = new MutationObserver((m) => observeMutations(m, visibilityObserver));
    domWatcher.observe(document.body, { childList: true, subtree: true });
  }

  function stopWatching() {
    updateButton(commentButton, "Włącz rozwijanie komentarzy");
    if (visibilityObserver) {
      visibilityObserver.disconnect();
      visibilityObserver = null;
    }
    if (domWatcher) {
      domWatcher.disconnect();
      domWatcher = null;
    }
  }

  function activateFocusMode() {
    const postContainer = findPostContainer();
    if (!postContainer) {
      alert("Nie udało się znaleźć kontenera posta");
      return;
    }
    updateButton(focusModeButton, "Wyłącz tryb skupienia", "darkorange");
    focusEnabled = true;
    document.body.classList.add("focus-hidden");
    panel.classList.add("focus-visible");
    postContainer.classList.add("focus-visible");
  }

  function deactivateFocusMode() {
    updateButton(focusModeButton, "Włącz tryb skupienia");
    focusEnabled = false;
    const focusElements = document.querySelectorAll(".focus-hidden, .focus-visible");
    focusElements.forEach((e) => e.classList.remove("focus-hidden", "focus-visible"));
  }

  function clickButton(button) {
    const textElements = [button, ...button.querySelectorAll("*")];
    setElementColors(textElements, "red");
    setTimeout(() => {
      button.click();
      clickedButtonsCount++;
      updateCounterDisplay();
    }, 500);
  }

  /* === OBSERVER FUNCTIONS === */

  function observeMutations(mutations, visibilityObserver) {
    mutations.forEach((m) => {
      m.addedNodes.forEach((n) => observeNode(n, visibilityObserver));
      m.removedNodes.forEach((n) => unobserveNode(n, visibilityObserver));
    });
  }

  function clickIntersectingEntries(entries) {
    entries.filter((e) => e.isIntersecting).forEach((e) => clickButton(e.target));
  }

  function observeNode(node, visibilityObserver) {
    const buttons = findMatchingButtons(node);
    buttons.forEach((b) => visibilityObserver.observe(b));
  }

  function unobserveNode(node, visibilityObserver) {
    const buttons = findMatchingButtons(node);
    buttons.forEach((b) => visibilityObserver.unobserve(b));
  }

  /* === UTILITIES === */

  function findPostContainer() {
    const headers = document.querySelectorAll("h2 span");
    const header = Array.from(headers).find((header) => /^Post /.test(header.textContent));
    if (!header) return;

    let container = header;
    for (let i = 0; i < FOCUS_LEVEL; i++) {
      container = container.parentElement;
      if (!container) return;
    }
    return container;
  }

  function isMatchingButton(element) {
    return (
      element.nodeType === Node.ELEMENT_NODE &&
      element.getAttribute?.("role") === "button" &&
      COMMENT_BUTTON_PATTERNS.some((pattern) => pattern.test(element.textContent))
    );
  }

  function findMatchingButtons(node) {
    if (node.nodeType !== Node.ELEMENT_NODE) return [];
    const matchingButtons = [];
    if (isMatchingButton(node)) matchingButtons.push(node);
    const descendantButtons = Array.from(node.querySelectorAll('div[role="button"]')).filter(
      isMatchingButton
    );
    matchingButtons.push(...descendantButtons);
    return matchingButtons;
  }

  /* === PANEL CONTROL FUNCTIONS === */

  function closePanel() {
    stopWatching();
    document.onmousemove = document.onmouseup = null;
    panel.remove();
  }

  function startDragging(event) {
    if (event.button !== 0) return; /* left click only */
    dragOffset = {
      x: event.clientX - panel.offsetLeft,
      y: event.clientY - panel.offsetTop,
    };
  }

  function dragPanel(event) {
    if (!dragOffset) return;
    panel.style.left = `${event.clientX - dragOffset.x}px`;
    panel.style.top = `${event.clientY - dragOffset.y}px`;
  }

  function stopDragging() {
    dragOffset = null;
  }

  /* === UI FUNCTIONS === */

  function createPanel() {
    const panelHTML = `
      <div id="${PANEL_ID}" style="
        position: fixed;
        top: 100px;
        left: 100px;
        background: #fff;
        border: 1px solid #000;
        padding: 8px;
        z-index: 999999;
        display: grid;
        gap: 5px;
      ">
        <h1 id="panel-title" style="cursor: move; text-align: center;">Asystent FB</h1>
        <button id="comment-button" style="cursor: pointer;">Włącz rozwijanie komentarzy</button>
        <span id="counter-display">Kliknięte przyciski: 0</span>
        <button id="focus-mode-button" style="cursor: pointer;">Włącz tryb skupienia</button>
        <button id="close-button" style="cursor: pointer;">Zamknij</button>
      </div>
    `;

    const tempDiv = document.createElement("div");
    tempDiv.innerHTML = panelHTML;
    return tempDiv.firstElementChild;
  }

  function updateButton(button, text, color) {
    button.textContent = text;
    button.style.backgroundColor = color ?? "black";
  }

  function updateCounterDisplay() {
    counterDisplay.textContent = `Kliknięte przyciski: ${clickedButtonsCount}`;
  }

  function setElementColors(elements, color) {
    elements.forEach((e) => e.style.setProperty("color", color, "important"));
  }
})();

PS: odkrycie dnia - kod skryptozakładki można pobierać dynamicznie z lokalne serwera, a samą ją odpalać taki prostym snippetem! Moje życie po raz kolejny stało się prostsze.

javascript: (function () {
  var s = document.createElement("script");
  s.src = "http://localhost:1111/script.js";
  document.body.appendChild(s);
})();