LinuxDo自定义🛠️

为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊、详情展开、页面加宽、发帖时间显示 等功能。

目前為 2024-08-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name LinuxDo自定义🛠️
// @name:en     LinuxDo Custom🛠️
// @name:zh-CN  LinuxDo自定义🛠️
// @description 为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊、详情展开、页面加宽、发帖时间显示 等功能。
// @description:en Adds customizable features such as logos, click count visualization, image resize, and quick bookmarking to LinuxDo
// @description:zh-CN 为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊、详情展开、页面加宽、发帖时间显示 等功能。
// @version      0.5.3
// @author       Yearly
// @match        https://linux.do/*
// @icon        data:image/svg+xml;base64,PHN2ZyAgdmlld0JveD0iMCAtMiAzNiAzNiIgdmVyc2lvbj0iMS4xIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KICA8cmVjdCB5PSItMiIgd2lkdGg9IjM2IiBoZWlnaHQ9IjM2IiByeD0iMTUlIiBmaWxsPSIjMDAwIi8+CiAgPHBhdGggZmlsbD0iI2VlZSIgIGQ9Ik0zMy42OCwxNS40SDIyLjIzQTMuNjgsMy42OCwwLDAsMSwxOSw5Ljg5bC40LS42OUg0VjdIMjAuNzFsMS4xNS0ySDRBMiwyLDAsMCwwLDIsN1YyOWEyLDIsMCwwLDAsMiwySDMyYTIsMiwwLDAsMCwyLTJWMTUuMzhaTTE2LjgsMTkuODNsLTEwLDQuNTlWMjEuNzhsNi41MS0zLTYuNTEtM1YxMy4xN2wxMCw0LjU5Wm02LjYsNS41N0gxN1YyM2g2LjRaIiBjbGFzcz0iY2xyLWktc29saWQtLWFsZXJ0ZWQgY2xyLWktc29saWQtcGF0aC0xLS1hbGVydGVkIi8+CiAgPHBhdGggZmlsbD0iI0ZEMSIgZD0iTTI2Ljg1LDEuMTQsMjEuMTMsMTFBMS4yOCwxLjI4LDAsMCwwLDIyLjIzLDEzSDMzLjY4QTEuMjgsMS4yOCwwLDAsMCwzNC43OCwxMUwyOS4wNiwxLjE0QTEuMjgsMS4yOCwwLDAsMCwyNi44NSwxLjE0WiIgY2xhc3M9ImNsci1pLXNvbGlkLS1hbGVydGVkIGNsci1pLXNvbGlkLXBhdGgtMi0tYWxlcnRlZCBjbHItaS1hbGVydCIvPgo8L3N2Zz4K
// @license      GPL-3.0
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @namespace    http://tampermonkey.net/
// @supportURL   https://gf.qytechs.cn/scripts/499029
// @homepageURL  https://gf.qytechs.cn/scripts/499029
// ==/UserScript==

(function() {
  var settings = {};

  const default_main_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtbGFiZWw9IkxpbnV4IiB2aWV3Qm94PSIwIDAgNTAwIDUwMCIgZmlsbD0iIzMzMyI+PHJlY3Qgd2lkdGg9IjQ1MCIgaGVpZ2h0PSI0NTAiIHJ4PSIxNSUiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNMTYxIDE5MGMtNiAxNC00OCA1OC00NCAxMDIgMTYgMTg0IDcyIDYwIDE1NiAxMDYgMCAwIDE1MC04NCAzMC0yMjAtMzQtNDgtNC04Ni0yNi0xMThzLTYwLTM0LTg4LTQgMTIgNzQtMjggMTM0Ii8+PHBhdGggZD0iTTMwOSAyODJzMTgtMzYtMTYtNjJjMzIgMzQgMTIgNjQgMTIgNjRoLTZjLTItNzAtMjAtMzItNDYtMTU2IDMwLTM0LTI4LTY0LTI4LThoLTE4YzItNDgtNDAtMjQtMTYgMTAtMiA3NC00NiAxMDQtNDYgMTU2LTE0LTM2IDEyLTY0IDEyLTY0cy0zNiAzMC0xNCA3NCA2MiAzNCAzNCA1NGM0NCAzMCAxMTIgMTAgMTEwLTU0IDItMTYgNDQtMTAgNDgtNnMtNi04LTI2LThNMTk3IDEyNmMtMTQtNC0xMC0yMi00LTIyczE2IDE0IDQgMjJtMzggMmMtMTAtMTQtMi0yOCA4LTI2czEwIDI2LTggMjYiIGZpbGw9IiNlZWUiLz48ZyBmaWxsPSIjZmMyIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMiI+PHBhdGggZD0ibTE0MyAzMDIgNDIgNjBjMjIgMTQgMTAgNzAtNTAgNDItMzQtMTAtNjItOC02Ni0yNnM4LTIwIDYtMjhjLTgtNDQgMjgtMjIgMzgtNDRzMTAtMzIgMzAtNG0yMjQgMjhjLTgtMTIgMC0zNC0yOC0zMi0xMiAyNC00NiA0OC00OCAwLTIwIDAtNiA0OC0xNCA3MC0xOCA1NCAzNCA1OCA1NiAzMmw1Mi0zNmM0LTYgMTAtMTItMTgtMzRNMTgzIDE0NmMtNi0xMiAyMi0yOCAzMi0yOHMyNCA4IDM4IDEyIDggMTggNCAyMC0yNiAyMC00MiAyMC0yMC0xNi0zMi0yNCIvPjxwYXRoIGQ9Ik0xODMgMTQ0YzE2IDEyIDM0IDIyIDcwLTYiLz48L2c+PHBhdGggZD0iTTIwNSAxMzJjLTQgMCAyLTQgNC0ybTE0IDJjMi0yLTItNC02LTIiLz48L3N2Zz4="

  const default_wide_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMTAiIGhlaWdodD0iMTEwIiB2aWV3Ym94PSItNSAtNSAzMTAgMTEwIj48cmVjdCB3aWR0aD0iOTkiIGhlaWdodD0iOTkiIHJ4PSIxMDAlIiBmaWxsPSIjRjFGMUYxIi8+PHRleHQgeD0iMjQiIHk9Ijc2IiBmb250LXNpemU9Ijc3IiBmaWxsPSIjRmMzIiBmb250LXdlaWdodD0iYm9sZCI+TDwvdGV4dD48dGV4dCB4PSI3MCIgeT0iNzUiIGZvbnQtc2l6ZT0iNDYiIGZpbGw9IiM4ODgiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZXR0ZXItc3BhY2luZz0iNSI+SU5VWDwvdGV4dD48dGV4dCB4PSIyMDUiIHk9Ijc3IiBmb250LXNpemU9IjcwIiBmaWxsPSIjRUVFIiBmb250LXdlaWdodD0iNjYwIj5EbzwvdGV4dD48L3N2Zz4=";

  const settingsConfig = {
    quick_mark    : { type: 'checkbox', label: '快速收藏  ', default: true, style:'', info:'在帖子上增加一个⭐用于快速收藏到书签' },
    cnts_colorful : { type: 'checkbox', label: '点击数可视化', default: true, style:'', info:'点击数彩色高亮,数越大,颜色越红' },
    image_view    : { type: 'checkbox', label: '增强大图查看', default: true, style:'', info:'在大图查看时,支持滚轮缩放和鼠标拖动位置' },
    spoiler_noblur: { type: 'checkbox', label: '去除模糊', default: false, style:'', info:'去除剧透字段的模糊,使其直接显示' },
    details_open  : { type: 'checkbox', label: '详情展开', default: false, style:'', info:'直接展开被折叠的详情' },
    wider_page    : { type: 'checkbox', label: '超宽显示', default: false, style:'', info:'让页面显示尽量宽' },
    thin_header   : { type: 'checkbox', label: '窄的顶栏', default: false, style:'', info:'让(Header)顶栏变窄' },
    topic_scroll  : { type: 'checkbox', label: '帖子限高', default: true, style:'', info:'帖子内容限高,太长的帖子会变成滚动查看的元素' },

    show_up_time  : { type: 'checkbox', label: '显示发帖时间', default: true, style:'', info:'话题列表的帖子显示创建/更新时间,老的帖子会褪色泛黄' },

    goto_top_end  : { type: 'checkbox', label: '一键顶部/底部', default: true, style:'', info:'右下角新增按钮,可点击到顶部/底部' },
    auto_read     : { type: 'checkbox', label: '自动阅读', default: true, style:'', info:'右下角新增按钮,可点击开始自动阅读' },

    order_created : { type: 'checkbox', label: '[新]按创建排序', default: true, style:'', info:'首页导航的[新]改成新创建排序' },

    image_mini    : { type: 'checkbox', label: '显示小图', default: false, style:'', info:'让帖子中的图都变小,在鼠标悬停时显示大图' },
    image_mini_H  : { type: 'number', label: '  小图高度', default: "70", dependsOn: 'image_mini', style:'font-size:15px; margin-top:10px;' , info:'(单位px,建议设为大于50的数)' },
    image_mini_W  : { type: 'number', label: '  小图宽度', default: "100", dependsOn: 'image_mini', style:'font-size:15px; margin-top:10px;' , info:'(单位px,建议设为大于50的数)' },

    icon_custom   : { type: 'checkbox', label: '自定义图标', default: false, style:'' , info:'始皇说不建议这样,所以我让鼠标悬停时能看眼原LOGO' },
    icon_main     : { type: 'text', label: '  主图标URL', default: default_main_icon, dependsOn: 'icon_custom', style:'font-size:15px; margin-top:10px;', info:'' },
    icon_wide     : { type: 'text', label: '  宽图标URL', default: default_wide_icon, dependsOn: 'icon_custom', style:'font-size:15px; margin-top:10px;', info:'' },


  };

  Object.keys(settingsConfig).forEach(key => {
    settings[key] = GM_getValue(key, settingsConfig[key].default);
  });

  GM_registerMenuCommand('Custom Settings', openSettings);


  function openSettings() {
    if (document.querySelector('div#linuxdo-custom-setting')) {
      return;
    }
    const shadow = document.createElement('div');
    shadow.style = `position: fixed; top: 0%; left: 0%; z-index:8888; width:100vw; height:100vh; background: #6668;`;
    const panel = document.createElement('div');
    panel.style = `position: fixed; top: 45%; left: 50%; z-index:9999; transform: translate(-50%, -50%); background: white; padding:15px 25px; border: 1px solid #ccc; color:#000;`;
    panel.id = "linuxdo-custom-setting"
    let html = `
            <style type="text/css">
              :scope label {color:#666; font-size:16px; display:flex; justify-content:space-between; align-items:center; margin-top:25px;}
              :scope label span {color:#6bc; font-size:12px; font-weight:normal; padding:0 6px; margin-right:auto;}
              :scope label input[type=text] {width:300px; padding:1px; margin:0 5px 0 0; font-size:14px;}
              :scope label input[type=number] {width:60px; padding:1px; margin:0 5px; text-align:center;}
              :scope label input[type=checkbox] {background:pink; margin:0 5px;}
              :scope label input[disabled] {background: #CCC;}
              :scope label button {user-select: none; color: #333; padding: 2px 10px; margin-top:10px; border-radius:5px;}
            </style>
            <h2 style="text-align:center; margin-top:.5rem;">———— Settings ————</h2>
            `;
    Object.keys(settingsConfig).forEach(key => {
      const cfg = settingsConfig[key];
      const val = settings[key];
      const checked = cfg.type === 'checkbox' && val ? 'checked' : '';
      const disabled = cfg.dependsOn && !settings[cfg.dependsOn] ? 'disabled' : '';
      html += `<label style="${cfg.style}">${cfg.label}<span>${cfg.info}</span><input type="${cfg.type}" id="ujs_set_${key}" value="${val}" ${checked} ${disabled} ></label>`;
    });
    html += `<label><button id="ld_userjs_apply" style="font-weight: bold; background:#ACE">保存并刷新</button>
           <span></span><button id="ld_userjs_save">仅保存</button>
           <span></span><button id="ld_userjs_reset">重置</button>
           <span></span><button id="ld_userjs_close">取消</button></label>`;
    panel.innerHTML = html;

    document.body.append(shadow, panel);

    Object.keys(settingsConfig).forEach(key => {
      if (settingsConfig[key].dependsOn) {
        document.getElementById(`ujs_set_${settingsConfig[key].dependsOn}`).addEventListener('change', updateDependencies);
      }
    });

    function updateDependencies() {
      Object.keys(settingsConfig).forEach(key => {
        if (settingsConfig[key].dependsOn) {
          document.getElementById(`ujs_set_${key}`).disabled = !document.getElementById(`ujs_set_${settingsConfig[key].dependsOn}`).checked;
        }
      });
    }

    document.querySelector('button#ld_userjs_save').addEventListener('click', () => {
      Object.keys(settingsConfig).forEach(key => {
        const element = document.getElementById(`ujs_set_${key}`);
        settings[key] = element.type === 'checkbox' ? element.checked : element.value;
        GM_setValue(key, settings[key]);
      });
      alert('Settings saved!');
      panel.remove();
    });

    document.querySelector('button#ld_userjs_apply').addEventListener('click', () => {
      Object.keys(settingsConfig).forEach(key => {
        const element = document.getElementById(`ujs_set_${key}`);
        settings[key] = element.type === 'checkbox' ? element.checked : element.value;
        GM_setValue(key, settings[key]);
      });
      window.location.reload();
    });

    document.querySelector('button#ld_userjs_reset').addEventListener('click', () => {
      Object.keys(settingsConfig).forEach(key => {
        GM_deleteValue(key);
      });
      window.location.reload();
    });

    function setting_hide() {
      panel.remove();
      shadow.remove();
    }


    document.querySelector('button#ld_userjs_close').addEventListener('click', () => setting_hide());

    shadow.onclick = () => setting_hide();

    updateDependencies();
  }

  // Function 1: Custom Logo
  if (settings.icon_custom) {
    GM_addStyle(`
            #site-logo {
                object-fit: scale-down;
                object-position: -999vw;
                background-size: cover;
                background-repeat: no-repeat;
                background-image: url('${settings.icon_main}');
                opacity: 1;
                transition: opacity 0.5s ease;
            }
            #site-logo.logo-big {
                background-image: url('${settings.icon_wide}');
            }
            #site-logo.logo-mobile {
                background-image: url('${settings.icon_wide}');
            }
            #site-logo:hover {
                object-position: unset;
                background-image: none;
            }
        `);

    function replaceIcon() {
      document.querySelector('link[rel="icon"]').href = settings.icon_main;
    }

    const observer = new MutationObserver(replaceIcon);
    observer.observe(document.head, { childList: true, subtree: true });

    replaceIcon();
  }

  // Function 2: Click Counts Visualization
  if (settings.cnts_colorful) {
    (function countsColorful() {
      const badges = document.querySelectorAll("span.badge.badge-notification.clicks");
      let values = Array.from(badges, badge => parseInt(badge.title || badge.textContent));
      let maxValue = Math.max(...values);
      let minValue = Math.min(...values);
      if (maxValue < 100 || (maxValue - minValue < 10)) maxValue = maxValue * 1.5;
      badges.forEach(badge => {
        if (!badge.style.backgroundColor) {
          const number = parseInt(badge.title || badge.textContent);
          const hue = 180 - (number / maxValue) * 180;
          badge.style.backgroundColor = `hsl(${hue}, 50%, 50%)`;
          badge.style.color = "#fff";
          const sl = document.createElement('span');
          sl.style = `height: 1em; display: inline-block; float: right; background: hsl(${hue}, 50%, 50%); width: ${100 * (number / maxValue)}px;`;
          badge.after(sl);
        }
      });
      setTimeout(countsColorful, 1500);
    })();
  }

  // Function 3: Image Resize and Drag
  if (settings.image_view) {
    let sizePercent = 80;
    let isDragging = false;
    let startX, startY, initialX, initialY;

    function adjustSize(event) {
      let contentImg = document.querySelector('section#discourse-lightbox img');
      if (contentImg) {
        let delta = event.deltaY > 0 ? -10 : 10;
        sizePercent += delta;
        if (sizePercent > 300) sizePercent = 300;
        if (sizePercent < 5) sizePercent = 5;

        contentImg.style.width = sizePercent + '%';
        contentImg.style.maxWidth = sizePercent + '%';
        contentImg.style.maxHeight = sizePercent + '200%';
      }
    }

    function startDrag(event) {
      let contentImg = document.querySelector('section#discourse-lightbox img');
      if (contentImg) {
        isDragging = true;
        startX = event.clientX;
        startY = event.clientY;
        initialX = contentImg.offsetLeft;
        initialY = contentImg.offsetTop;
        event.preventDefault();
      }
    }

    function drag(event) {
      if (isDragging) {
        let contentImg = document.querySelector('section#discourse-lightbox img');
        if (contentImg) {
          let dx = event.clientX - startX;
          let dy = event.clientY - startY;
          contentImg.style.left = (initialX + dx) + 'px';
          contentImg.style.top = (initialY + dy) + 'px';
        }
      }
    }

    function stopDrag(event) {
      isDragging = false;
    }

    let observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        mutation.addedNodes.forEach(function(node) {
          let contentImg = document.querySelector('section#discourse-lightbox img');
          if (contentImg) {
            document.querySelector('section#discourse-lightbox').onwheel = adjustSize;
            contentImg.onmousedown = startDrag;
            contentImg.onmouseup = stopDrag;
            contentImg.onmousemove = drag;
            contentImg.style.cursor = "move";

            function stopClickEvent(event) {
              event.stopImmediatePropagation();
              event.preventDefault();
            }
            contentImg.addEventListener('click', stopClickEvent, true);
          }
        });
      });
    });

    observer.observe(document.body, { childList: true, subtree: true });

  }

  // Function 4: Quick Bookmark
  if (settings.quick_mark) {
    const starSvg = `<svg class="svg-icon" aria-hidden="true" style="text-indent: 1px; transform: scale(1); width:18px; height:18px;">
             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
             <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path></svg></svg> `;
    let markMap = new Map();

    function handleResponse(xhr, successCallback, errorCallback) {
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
          if (xhr.status === 200) {
            successCallback(xhr);
          } else {
            errorCallback(xhr);
          }
        }
      };
    }

    function deleteStarMark(mark_btn, data_id) {
      if (markMap.has(data_id)) {
        const mark_id = markMap.get(data_id);
        var xhr = new XMLHttpRequest();
        xhr.open('DELETE', `/bookmarks/${mark_id}`, true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
        xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);

        handleResponse(xhr, (xhr) => {
          mark_btn.style.color = '#777';
          mark_btn.title = "收藏";
          mark_btn.onclick = () => addStarMark(mark_btn, data_id);
        }, (xhr) => {
          alert('删除失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
        });

        xhr.send();
      }
    }

    function TryParseJson(str) {
      try {
        const jsonObj = JSON.parse(str);
        return JSON.stringify(jsonObj, null, 1);
      } catch (error) {
        return str;
      }
    }

    function addStarMark(mark_btn, data_id) {
      const xhr = new XMLHttpRequest();
      xhr.open('POST', '/bookmarks', true);
      xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
      xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
      xhr.setRequestHeader('discourse-logged-in', ' true');
      xhr.setRequestHeader('discourse-present', ' true');
      xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
      const postData = `name=%E6%94%B6%E8%97%8F&auto_delete_preference=3&bookmarkable_id=${data_id}&bookmarkable_type=Post`;

      handleResponse(xhr, (xhr) => {
        mark_btn.style.color = '#fdd459';
        mark_btn.title = "删除收藏";
        mark_btn.onclick = () => deleteStarMark(mark_btn, data_id);
      }, (xhr) => {
        alert('收藏失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
      });

      xhr.send(postData);
    }

    function addMarkBtn() {
      let articles = document.querySelectorAll("article[data-post-id]");
      if (articles.length <= 0) return;

      articles.forEach(article => {
        const target = article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > div.actions");
        if (target && !article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > span.star-bookmark")) {
          const dataPostId = article.getAttribute('data-post-id');
          const starButton = document.createElement('span');

          starButton.innerHTML = starSvg;
          starButton.className = "star-bookmark";
          starButton.style.cursor = 'pointer';
          starButton.style.margin = '0px 12px';

          if (markMap.has(dataPostId)) {
            starButton.style.color = '#fdd459';
            starButton.title = "删除收藏";
            starButton.onclick = () => deleteStarMark(starButton, dataPostId);
          } else {
            starButton.style.color = '#777';
            starButton.title = "收藏";
            starButton.onclick = () => addStarMark(starButton, dataPostId);
          }
          target.after(starButton);
        }
      });
    }

    function getStarMark() {
      let articles = document.querySelectorAll("article[data-post-id]");
      if (articles.length <= 0) return;

      const currentUserElement = document.querySelector('#current-user button > img[src]');

      function extractUsername(srcString) {
        const regex = /\/user_avatar\/linux\.do\/([^\/]+)\/\d+\//;
        const match = srcString.match(regex);

        if (match && match[1]) {
          return match[1];
        } else {
          return null;
        }
      }

      if(!currentUserElement) return;

      const currentUsername = extractUsername(currentUserElement.getAttribute('src'));

      if(!currentUsername) return;

      const xhr = new XMLHttpRequest();
      xhr.open('GET', `/u/${currentUsername}/user-menu-bookmarks`, true);
      xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);

      handleResponse(xhr, (xhr) => {
        var response = JSON.parse(xhr.responseText);
        response.bookmarks.forEach(mark => {
          markMap.set(mark.bookmarkable_id.toString(), mark.id.toString());
        });
        addMarkBtn();
      }, (xhr) => {
        console.error('GET请求失败:', xhr.statusText);
      });

      xhr.send();
    }

    let lastUpdateMarkTime = 0;
    let lastUpdateButnTime = 0;
    function mutationCallback() {
      const currentTime = Date.now();
      if (currentTime - lastUpdateMarkTime > 9000) {
        setTimeout(getStarMark, 500);
        lastUpdateMarkTime = currentTime;
      }
      if (currentTime - lastUpdateButnTime > 1000) {
        setTimeout(addMarkBtn, 500);
        lastUpdateButnTime = currentTime;
      }
    }

    const mainNode = document.querySelector("#main-outlet");
    if (mainNode) {
      const observer = new MutationObserver(mutationCallback);
      observer.observe(mainNode, { childList: true, subtree: true });
    }

    getStarMark();
  }

  // Function 5: mini article image show
  if (settings.image_mini) {
    let _H = parseInt(settings.image_mini_H);
    let _W = parseInt(settings.image_mini_W);//  transition: max-width 0.5s ease-in-out, max-height 0.5s ease-in-out;

    GM_addStyle(`
        article div.topic-body div.regular.contents img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {
           max-width : ${_W}px;
           max-height : ${_H}px;
           object-fit: contain;
        }
        `)

    var imageMiniTimer = setInterval(function() {
      var images = document.querySelectorAll('article div.topic-body div.regular.contents img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji)');

      if (images.length >= 1) {

        for (var i = 0; i < images.length; i++) {
          let img = images[i];
          let image_src = null;
          let src_height = null;

          let urls = img.getAttribute('srcset')
          if (urls) {
            urls = urls.match(/https:\/\/[^,\s]+/g);
            image_src = urls[urls.length - 1];
          }else{
            image_src = img.src;
          }

          src_height = img.naturalHeight || img.height;

          if (img.parentElement.matches('a.lightbox')) {
            img = img.parentElement;
            if (!image_src) {
              image_src = img.getAttribute('href');
            }
          }
          //console.log(image_src)
          img.image_src = image_src;
          img.src_height = src_height;

          let previewDiv = null;

          if (document.getElementById('hover-preview-img') == null) {
            previewDiv = document.createElement('div');
            previewDiv.id = 'hover-preview-img';
            previewDiv.style = 'position: fixed; z-index:999; top:-10px; max-width: 0px; max-height 0px; opacity: 0; transition: max-width 0.3s ease-in-out, max-height 0.3s ease-in-out, left 0.3s ease-in-out , opacity 0.3s ease-in-out , top 0.3s ease-in-out;'; // display:none;
            document.body.appendChild(previewDiv);
            let fullSizeImg = document.createElement('img');
            fullSizeImg.className = 'full-size-image';
            previewDiv.appendChild(fullSizeImg);
          } else {
            previewDiv = document.getElementById('hover-preview-img');
          }

          img.addEventListener('mouseenter', function(event) {
            let previewDiv = document.getElementById('hover-preview-img');
            let fullSizeImg = previewDiv.querySelector('.full-size-image');
            previewDiv.style.display = 'block';
            previewDiv.style.background="#FFFE";
            previewDiv.style.boxShadow="1px 1px 5px #555";
            previewDiv.style.padding="0px";

            previewDiv.style.left= event.clientX + 20 + 'px';
            previewDiv.style.maxWidth = '99vw';
            previewDiv.style.maxHeight = '99vh';

            this.title="";
            fullSizeImg.src = this.image_src;
            fullSizeImg.style.width = '';
            fullSizeImg.style.height = '';
            fullSizeImg.style.maxWidth = '100%';
            fullSizeImg.style.maxHeight = '100%';
            previewDiv.style.top = event.clientY - this.src_height/2 + 'px';
            previewDiv.style.opacity = 1;

            fullSizeImg.onload = function() {
              console.log(previewDiv.offsetTop , fullSizeImg.naturalHeight , window.innerHeight);
              if (previewDiv.offsetTop + fullSizeImg.naturalHeight > window.innerHeight - 5) {
                previewDiv.style.top = window.innerHeight - 5 - fullSizeImg.naturalHeight + 'px';
              }
            };

          });

          img.addEventListener('mouseleave', function() {
            let previewDiv = document.getElementById('hover-preview-img');
            previewDiv.style.top = "-10px";
            previewDiv.style.maxWidth = "0px";
            previewDiv.style.maxHeight = "0px";
            previewDiv.style.opacity = 0;
          });
        }
      }
    }, 1000);
  }

  // Function 6: remove spoiler blurred
  if (settings.spoiler_noblur) {
    GM_addStyle(`
        .spoiler-blurred {
          filter: drop-shadow(0px 0px 3px #BBB)!important;
        }

        .spoiler-blurred img {
          filter: drop-shadow(0px 0px 3px #BBB)!important;
        }

        `)
  }

  // Function 7: details open
  if (settings.details_open) {
    function open_detail() {
      let details = document.querySelectorAll("article details");
      details.forEach(detail => {
        if (detail.opened != true) {
          detail.open = true;
          detail.opened = true;
        }
      });
      setTimeout(open_detail, 990);
    }
    setTimeout(open_detail, 900);
  }

  // Function 8: wider page
  if (settings.wider_page) {
    GM_addStyle(`
        #main-outlet-wrapper {
          max-width: 100%!important;
        }
        body.has-sidebar-page header.d-header > div.wrap {
          max-width: 100%!important;
        }
        .topic-body {
          width: 100%!important;
        }
        :root {
          --d-max-width: 100%!important;
        }
        article .topic-map.--op {
          max-width: 100%;
        }
        @media screen and (min-width: 925px) {
           #main-outlet .container.posts {
              grid-template-columns: auto 120px;
           }
        }
        `)
  }

  // Function 9: thin_header
  if (settings.thin_header) {
    GM_addStyle(`
        .d-header  {
          height: 2.5em !important;
        }
        .d-header .extra-info-wrapper .title-wrapper {
          display: flex;
          flex-direction: row;
        }
        .d-header div.title-wrapper > h1.header-title {
          width: auto;
          font-size: large;
        }
        .d-header #site-logo {
           height: 2em !important;
        }
        .d-header .d-header-icons .icon img.avatar {
           height: 2em !important;
        }
        `)
  }

  // Function 10: topic contents scroll
  if (settings.topic_scroll) {
    GM_addStyle(`
        article div.topic-body .regular.contents .cooked {
            max-height: 60vh;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: #aaaa #1111;
        }
        article div.topic-body .regular.contents .cooked ::-webkit-scrollbar-track {
            background: #1111;
        }
        article div.topic-body .regular.contents .cooked ::-webkit-scrollbar-thumb {
            background: #aaaa;
        }
        article div.topic-body .regular.contents .cooked ::-webkit-scrollbar-thumb:hover {
            background: #0008;
        }
        `)
  }

  // Function 11: order by Created
  if (settings.order_created) {
    function orderByCreated() {
      const a_new = document.querySelector("ul#navigation-bar > li.new.ember-view.nav-item_new > a");
      if ( a_new && a_new.href.endsWith("/new")) {
        a_new.parentNode.title = "按最新创建排序";
        a_new.href = a_new.href.replace("/new","/latest?order=created");
        a_new.style.filter="drop-shadow(0px 0px 1px var(--quaternary))"; // #8FF8
      }
      setTimeout(orderByCreated, 990);
    }
    setTimeout(orderByCreated, 900);
  }

  // Function 12: 显示发帖时间和最新回复时间
  if (settings.show_up_time) {

    function getHue(date, currentDate) {
      const diff = Math.abs(currentDate - date);
      const baseday = 30 * 24 * 60 * 60 * 1000; // 30 day
      const diffRatio = Math.min( Math.log(diff / baseday + 1), 1);
      return 120 - (140 * diffRatio); // green to red
    }

    function formatDate(date) {
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const day = String(date.getDate()).padStart(2, '0');
      const hours = String(date.getHours()).padStart(2, '0');
      const minutes = String(date.getMinutes()).padStart(2, '0');

      return `${year} ${month} ${day} ${hours}:${minutes}`;
    }

    function parseDate(dateStr) {
      let parts;

      // Check if the string is in Chinese format
      if (dateStr.match(/(\d+) 年 (\d+) 月 (\d+) 日 (\d+):(\d+)/)) {
        parts = dateStr.match(/(\d+) 年 (\d+) 月 (\d+) 日 (\d+):(\d+)/);
        return new Date(parts[1], parts[2] - 1, parts[3], parts[4], parts[5]);
      }

      // Check if the string is in English format
      if (dateStr.match(/(\w+) (\d+), (\d+) (\d+):(\d+) (am|pm)/i)) {
        parts = dateStr.match(/(\w+) (\d+), (\d+) (\d+):(\d+) (am|pm)/i);
        const monthMap = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
        let hour = parseInt(parts[4], 10);
        if (parts[6].toLowerCase() === 'pm' && hour < 12) {
          hour += 12;
        } else if (parts[6].toLowerCase() === 'am' && hour === 12) {
          hour = 0;
        }
        return new Date(parts[3], monthMap[parts[1]], parts[2], hour, parts[5]);
      }

      throw new Error('Unsupported date format');
    }

    GM_addStyle(`
        .topic-list .topic-list-data.age.activity {
          width: 11em;
          padding: 0px 5px;
        }
        .topic-list .topic-list-data.age.activity > a.post-activity{
          font-size: 14px;
          text-align: left;
          display: block;
          text-wrap: nowrap;
          padding: 8px 5px;
        }`);

    function creatTimeShow() {
      document.querySelectorAll(".num.topic-list-data.age.activity").forEach(function (item) {
        const timeSpan = item.querySelector("a.post-activity")
        if (timeSpan.innerText.length > 10) {
          return;
        }
        const timeInfo = item.title;
        let createDateString = timeInfo.match(/创建日期:([\d 年 月 日 :]+)/) || timeInfo.match(/Created: ([\w, \d:apm ]+)/);
        let updateDateString = timeInfo.match(/最新:([\d 年 月 日 :]+)/) || timeInfo.match(/Latest: ([\w, \d:apm ]+)/);
        createDateString = (createDateString[1] ?? '').trim();
        const createDate = parseDate(createDateString);
        const currentDate = new Date();
        const createHue = getHue(createDate, currentDate);
        const formatCreateDate = formatDate(createDate);
        timeSpan.innerHTML = `<span style="color: hsl(${createHue}, 35%, 50%);">创建:${formatCreateDate}</span><br>`;
        if (updateDateString) {
          updateDateString = (updateDateString[1] ?? '').trim();
          const updateDate = parseDate(updateDateString);
          const updateHue = getHue(updateDate, currentDate);
          const formatNewDate = formatDate(updateDate);
          timeSpan.innerHTML += `<span style="color: hsl(${updateHue}, 35%, 50%);">最新:${formatNewDate}</span>`
        } else {
          timeSpan.innerHTML += `<span style="color:#888;">最新:暂无回复</span>`
        }

        const pastDays = Math.abs(createDate - currentDate) / (24 * 60 * 60 * 1000);
        const topicTitle = item.parentNode.querySelector(".main-link")
        const topicUsers = item.parentNode.querySelector(".topic-list-data.posters")
        if ( pastDays > 30) {
          topicTitle.style.opacity = 0.9;
          topicTitle.style.filter = "grayscale(10%) sepia(10%)";
          topicUsers.style.filter = "grayscale(10%) sepia(10%)";
          if ( pastDays > 60) {
            topicTitle.style.opacity = 0.8;
            topicTitle.style.filter = "sepia(40%) brightness(85%)";
            topicUsers.style.filter ="sepia(40%) brightness(85%)";
          }
          if ( pastDays > 120) {
            topicTitle.style.filter = "sepia(90%) brightness(85%)";
            topicUsers.style.filter = "sepia(90%) brightness(85%)";
          }
        }
      })
      setTimeout(creatTimeShow, 990);
    }
    setTimeout(creatTimeShow, 900);
  }

  // Function 13: 快速点击到顶部/底部
  if (settings.goto_top_end) {

    function scrollTimeline(offset) {
      let element = document.querySelector(".timeline-padding");

      if (!element) {
        window.scrollTo({
          top: 9999 * offset,
          behavior: "smooth"
        });
        if (window.location.href.includes("/t/topic/")) {
          window.location.href = window.location.href.replace(/\/t\/topic\/(\d+)\/\d+/, `/t/topic/$1/${offset*999}`)
        }
        return;
      }

      const event = new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        clientX: element.getBoundingClientRect().left + 0,
        clientY: element.getBoundingClientRect().top + offset,
      });

      element.dispatchEvent(event);
    }

    var toTop = document.createElement("button");
    var toEnd = document.createElement("button");
    var scrollBtns = document.createElement("div");

    scrollBtns.className = "goto_top_end";
    toTop.innerText = "⬆️顶部";
    toEnd.innerText = "⬇️最新";

    GM_addStyle(`
    .goto_top_end{
       position: fixed; bottom: 5px; right: 10px; z-index: 1000; border:none;
    }
    .goto_top_end > button {
       background-color:#0005;
       color:#eee;
       border:none;
       padding: 10px;
       margin: 0px 5px;
       border-radius: 5px;
       cursor: pointer;
    }
    @media screen and (max-width: 924px) {
      .goto_top_end{
        right: 160px;
      }
    }
    `);

    toTop.onclick = function() {
      scrollTimeline(0)
    };

    toEnd.onclick = function() {
      scrollTimeline(500)
    };

    document.body.appendChild(scrollBtns);
    scrollBtns.append(toTop, toEnd);

  }

  GM_addStyle(`
  .topic-list td.topic-list-data.posters {
    height: auto;
    padding: 0.33em;
    width: 110px;
  }
  .topic-list td.posters.topic-list-data > a:first-child:not([style*="display: none"]) > img {
    width:  48px;
    height: 48px;
  }
  @media screen and (max-width: 850px) {
    .topic-list .topic-list-data.posters a.latest > img {
       width:  48px;
       height: 48px;
    }
    .topic-list td.topic-list-data.posters {
       width: 52px;
    }
  }
  `);

  function fristAvatarBigger() {
    document.querySelectorAll(`.topic-list td.posters.topic-list-data > a:first-child > img,
    .topic-list td.posters.topic-list-data > a.latest > img`).forEach(function (img) {
      if (img.src.includes("/24/")) {
        img.src = img.src.replace("/24/","/48/");
      }
    });
    setTimeout(fristAvatarBigger, 1990);
  }
  setTimeout(fristAvatarBigger, 900);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址