Miniflux thumbnails

Show thumbnails in entry listing

// ==UserScript==
// @name        Miniflux thumbnails
// @namespace   Violentmonkey Scripts
// @description Show thumbnails in entry listing
// @match       *://reader.miniflux.app/*
// @grant       none
// @version     1.1
// @author      -
// @license     WTFPL
// @inject-into content
// ==/UserScript==

(() => {
  "use strict"

  const itemList = document.querySelector(".items");

  async function getEntryData(entryId) {
    // load the entry using history route because it doesn't tamper with read status
    const entryUrl = new URL(`/history/entry/${entryId}`, location);
    const response = await fetch(entryUrl, {
      headers: {'Content-Type': 'text/html'}
    });
    const html = await response.text();
    const parser = new DOMParser();
    return parser.parseFromString(html, "text/html");
  }

  function getThumbnailUrl(dom) {
    const imageEnclosure = dom.querySelector('.enclosure-image img');
    console.log(imageEnclosure);
    if (imageEnclosure) {
      return imageEnclosure.src;
    }

    const img = dom.querySelector(".entry-content img");
    if (img) {
      return img.src;
    }

    return null;
  }

  function createThumbnailElement(thumbnailUrl) {
    const thumbnailElement = document.createElement("div");
    thumbnailElement.classList.add("entry-thumbnail");
    thumbnailElement.style.float = "right";

    const thumbnailImage = document.createElement("img");
    thumbnailImage.src = thumbnailUrl;
    thumbnailImage.alt = "Thumbnail";
    thumbnailImage.style.maxWidth = "64px";
    thumbnailImage.style.maxHeight = "64px";
    thumbnailImage.style.paddingLeft = "10px";

    const enlargedImage = document.createElement("div");
    enlargedImage.classList.add("enlarged-image");
    enlargedImage.style.display = "none";
    enlargedImage.style.position = "absolute";
    enlargedImage.style.zIndex = "9999";
    enlargedImage.style.backgroundColor = "white";
    enlargedImage.style.padding = "10px";
    enlargedImage.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)";

    const enlargedImageElement = document.createElement("img");
    enlargedImageElement.src = thumbnailUrl;
    enlargedImageElement.style.maxWidth = "700px";
    enlargedImageElement.style.maxHeight = "700px";

    enlargedImage.appendChild(enlargedImageElement);

    thumbnailElement.addEventListener("mouseenter", (event) => {
      const rect = event.target.getBoundingClientRect();
      const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
      const scrollY = window.pageYOffset || document.documentElement.scrollTop;

      enlargedImage.style.display = "block";
      enlargedImage.style.top = `${rect.top + scrollY}px`;
      enlargedImage.style.right = `${window.innerWidth - rect.left}px`;
    });

    thumbnailElement.addEventListener("mouseleave", () => {
      enlargedImage.style.display = "none";
    });

    thumbnailElement.appendChild(thumbnailImage);
    thumbnailElement.appendChild(enlargedImage);

    return thumbnailElement;
  }

  async function addThumbnailsToEntries() {
    const entries = itemList.querySelectorAll(".entry-item");

    const promises = Array.from(entries).map(async (entry) => {
      const entryId = entry.dataset.id;
      const entryData = await getEntryData(entryId);
      const thumbnailUrl = getThumbnailUrl(entryData);

      if (thumbnailUrl) {
        const thumbnailElement = createThumbnailElement(thumbnailUrl);
        entry.insertAdjacentElement("afterbegin", thumbnailElement);
      }
    });

    await Promise.all(promises);
  }

  addThumbnailsToEntries();
})();