Miniflux thumbnails

Show thumbnails in entry listing

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Miniflux thumbnails
// @namespace   Violentmonkey Scripts
// @description Show thumbnails in entry listing
// @match       *://reader.miniflux.app/*
// @grant       none
// @version     1.2
// @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');
    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;

      const viewportHeight = window.innerHeight;
      const isNearBottom = rect.bottom > viewportHeight / 2;

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

      if(isNearBottom) {
          enlargedImage.style.top = `${rect.bottom + scrollY - enlargedImage.offsetHeight}px`
      } else {
        enlargedImage.style.top = `${rect.top + scrollY}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();
})();