LeetCodeRating|English

LeetCodeRating The score of the weekly competition is displayed, and currently supports the tag page, question bank page, problem_list page and question page

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         LeetCodeRating|English
// @namespace    https://github.com/zhang-wangz
// @version      2.0.1
// @license      MIT
// @description  LeetCodeRating The score of the weekly competition is displayed, and currently supports the tag page, question bank page, problem_list page and question page
// @author       小东是个阳光蛋(Leetcode Nickname of chinese site
// @leetcodehomepage   https://leetcode.cn/u/runonline/
// @homepageURL  https://github.com/zhang-wangz/LeetCodeRating
// @contributionURL https://www.showdoc.com.cn/2069209189620830
// @match        *://*leetcode.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @connect      zerotrac.github.io
// @connect      raw.staticdn.net
// @connect      raw.gitmirror.com
// @connect      raw.githubusercontents.com
// @connect      raw.githubusercontent.com
// @require      https://gcore.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://gcore.jsdelivr.net/gh/andywang425/BLTH@4368883c643af57c07117e43785cd28adcb0cb3e/assets/js/library/layer.min.js
// @resource css https://gcore.jsdelivr.net/gh/andywang425/BLTH@d25aa353c8c5b2d73d2217b1b43433a80100c61e/assets/css/layer.css
// @grant        unsafeWindow
// @noframes
// @run-at       document-end
// @note         2022-12-29 1.1.0 add english site support
// @note         2022-12-29 1.1.1 fix when the dark mode is turned on, the prompt display is abnormal
// @note         2023-01-05 1.1.2 modify the cdn access address
// @note         2023-08-05 1.1.3 remaintain the project
// @note         2023-09-20 1.1.4 fix the error that scores are not displayed properly due to ui changes in problem page
// @note         2023-12-14 1.1.5 fix the error that scores are not displayed properly due to ui changes in problem set page
// @note         2025-08-21 2.0.0 refactor the plugin, change the refresh and update logic
// @note         2026-04-15 2.0.1 fix problem page rating display after LeetCode UI update; fix rating not shown on first install or daily first data fetch
// ==/UserScript==

(function () {
  "use strict"
  let t2rate = {}
  const version = "2.0.1"
  const DEBUG_MODE = false

  if (!DEBUG_MODE) {
    try {
      console.log = () => {}
    } catch (e) {}
  }

  let preDate
  const allProblemsUrl = "https://leetcode.com/problemset" // the problems page, contains all problems
  const problemListUrl = "https://leetcode.com/problem-list" // the problem list page, such as "https://leetcode.com/problem-list/array/"
  const problemUrl = "https://leetcode.com/problems/" // the specific problem page, such as "https://leetcode.com/problems/two-sum/description/"
  GM_addStyle(GM_getResourceText("css"))

  let lastProcessedProblemId

  function replaceDifficultyWithRating(difficultyLabel) {
    // 从难度标签向上遍历,找到包含题号的祖先行
    let row = difficultyLabel.parentElement
    while (row && !row.querySelector(".text-body .ellipsis")) {
      row = row.parentElement
      if (!row || row === document.body) return
    }
    const titleEl = row.querySelector(".text-body .ellipsis")
    if (!titleEl) return
    const match = (titleEl.textContent || "").match(/^(\d+)\.\s/)
    if (!match) return
    const problemIndex = match[1]
    if (t2rate[problemIndex] !== undefined) {
      difficultyLabel.textContent = t2rate[problemIndex].Rating
    }
  }

  function getAllProblemsData() {
    console.log(
      "[LeetCodeRating] getAllProblemsData() - " +
        new Date().toLocaleTimeString()
    )
    try {
      const difficultyLabels = document.querySelectorAll(
        'p[class*="text-sd-easy"], p[class*="text-sd-medium"], p[class*="text-sd-hard"]'
      )
      if (difficultyLabels.length === 0) return

      // 用已处理标记避免重复操作
      let unprocessed = 0
      for (const label of difficultyLabels) {
        if (!label.dataset.lcRatingProcessed) unprocessed++
      }
      if (unprocessed === 0) return

      console.log(
        `[LeetCodeRating] Found ${difficultyLabels.length} difficulty labels, ${unprocessed} unprocessed`
      )
      for (const label of difficultyLabels) {
        if (label.dataset.lcRatingProcessed) continue
        replaceDifficultyWithRating(label)
        label.dataset.lcRatingProcessed = "1"
      }
    } catch (e) {
      console.error("[LeetCodeRating] getAllProblemsData error:", e)
      return
    }
  }

  function getProblemListData() {
    console.log(
      "[LeetCodeRating] getProblemListData() - " +
        new Date().toLocaleTimeString()
    )
    // problem-list 页面与 problemset 页面使用相同的难度标签结构
    getAllProblemsData()
  }

  function getProblemData() {
    console.log(
      "[LeetCodeRating] getProblemData() - " +
        new Date().toLocaleTimeString()
    )
    try {
      // 从 URL 提取题目 slug,再从页面标题链接中提取题号
      const problemLinks = document.querySelectorAll('a[href^="/problems/"]')
      let problemIndex = null
      for (const link of problemLinks) {
        const match = (link.textContent || "").match(/^(\d+)\.\s/)
        if (match) {
          problemIndex = match[1]
          break
        }
      }

      if (problemIndex == null) {
        return
      }

      if (lastProcessedProblemId === problemIndex) {
        return
      }

      const colorSpan = document.querySelector(
        '[class*="text-difficulty-easy"], [class*="text-difficulty-medium"], [class*="text-difficulty-hard"]'
      )
      if (!colorSpan) return

      if (t2rate[problemIndex] !== undefined) {
        console.log(
          `[LeetCodeRating] Found rating for problem ${problemIndex}: ${t2rate[problemIndex].Rating}`
        )
        colorSpan.textContent = t2rate[problemIndex].Rating
      } else {
        console.log(
          `[LeetCodeRating] No rating found for problem ${problemIndex}, restoring original difficulty`
        )
        const classList = colorSpan.getAttribute("class") || ""
        if (classList.includes("text-difficulty-easy")) colorSpan.textContent = "Easy"
        else if (classList.includes("text-difficulty-medium")) colorSpan.textContent = "Medium"
        else if (classList.includes("text-difficulty-hard")) colorSpan.textContent = "Hard"
      }

      lastProcessedProblemId = problemIndex
    } catch (e) {
      console.error("[LeetCodeRating] getProblemData error:", e)
      return
    }
  }

  t2rate = JSON.parse(GM_getValue("t2ratedb", "{}").toString())
  console.log(
    `[Data Init] Loaded t2rate from storage, keys count: ${
      Object.keys(t2rate).length
    }`
  )

  preDate = GM_getValue("preDate", "")
  const now = new Date().toISOString().slice(0, 10)

  const t2rateInitialized = GM_getValue("t2rateInitialized", "")

  console.log(
    `[Data Init] preDate: ${preDate}, now: ${now}, initialized: ${t2rateInitialized !== ""}`
  )

  if (t2rateInitialized === "" || preDate !== now) {
    console.log(`[Data Init] Need to fetch new data from server`)

    GM_xmlhttpRequest({
      method: "get",
      url:
        "https://raw.githubusercontent.com/zerotrac/leetcode_problem_rating/main/data.json" +
        "?timeStamp=" +
        new Date().getTime(),
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      onload: function (res) {
        if (res.status !== 200) {
          console.log(`[Data Init] Failed to fetch data, status: ${res.status}`)
          return
        }
        console.log(`[Data Init] Successfully fetched data from server`)
        try {
          const dataStr = res.response
          const json = JSON.parse(dataStr)
          console.log(`[Data Init] Parsed ${json.length} problem records`)

          // 暂存到 newRate,解析或处理失败时不破坏已有缓存
          const newRate = {}
          for (const element of json) {
            newRate[element.ID] = element
            newRate[element.ID].Rating = Math.round(parseFloat(element.Rating))
          }
          t2rate = newRate
          console.log(
            `[Data Init] Processed t2rate, final keys count: ${
              Object.keys(t2rate).length
            }`
          )
          preDate = now
          GM_setValue("preDate", preDate)
          GM_setValue("t2ratedb", JSON.stringify(t2rate))
          GM_setValue("t2rateInitialized", "1")
          // 重置状态,确保当前页面用新数据重新处理
          lastProcessedProblemId = undefined
          document.querySelectorAll('[data-lc-rating-processed]').forEach(el => {
            delete el.dataset.lcRatingProcessed
          })
          tryProcess()
        } catch (e) {
          console.error("[Data Init] Failed to parse/process data:", e)
          console.error(
            "[Data Init] Response snippet:",
            (res.response || "").slice(0, 200)
          )
        }
      },
      onerror: function (err) {
        console.error("[Data Init] Failed to fetch data:", err)
      },
    })
  }

  // ==================== 页面处理调度 ====================

  // 根据当前 URL 判断页面类型
  function getPageType() {
    const url = location.href
    if (url.startsWith(allProblemsUrl)) return "allProblems"
    if (url.startsWith(problemListUrl)) return "problemList"
    if (url.startsWith(problemUrl)) return "problem"
    return null
  }

  const pageFuncs = {
    allProblems: getAllProblemsData,
    problem: getProblemData,
    problemList: getProblemListData,
  }

  function tryProcess() {
    const pageType = getPageType()
    if (pageType && pageFuncs[pageType]) {
      pageFuncs[pageType]()
    }
  }

  // ==================== URL 变化检测 ====================
  // Monkey-patch history.pushState / replaceState 以检测 SPA 导航

  let lastUrl = location.href
  const origPushState = history.pushState
  const origReplaceState = history.replaceState

  function onUrlChange() {
    const newUrl = location.href
    if (newUrl !== lastUrl) {
      console.log(`[UrlChange] ${lastUrl} -> ${newUrl}`)
      lastUrl = newUrl
      // 重置缓存,确保新页面内容会被处理
      document.querySelectorAll('[data-lc-rating-processed]').forEach(el => {
        delete el.dataset.lcRatingProcessed
      })
      lastProcessedProblemId = undefined
      tryProcess()
    }
  }

  history.pushState = function (...args) {
    const result = origPushState.apply(this, args)
    onUrlChange()
    return result
  }

  history.replaceState = function (...args) {
    const result = origReplaceState.apply(this, args)
    onUrlChange()
    return result
  }

  window.addEventListener("popstate", onUrlChange)

  // ==================== MutationObserver ====================
  // 监听 DOM 变化,debounce 后处理(处理 URL 变化后 React 异步渲染的情况)

  let debounceTimer = null
  const observer = new MutationObserver(() => {
    if (debounceTimer) clearTimeout(debounceTimer)
    debounceTimer = setTimeout(tryProcess, 300)
  })
  observer.observe(document.body, { childList: true, subtree: true })

  // ==================== 其他初始化 ====================

  document.addEventListener('copy', e => e.stopPropagation(), true)

  // 版本更新机制 (仅在主页检查)
  if (window.location.href.startsWith(allProblemsUrl)) {
    GM_xmlhttpRequest({
      method: "get",
      url:
        "https://raw.githubusercontent.com/zhang-wangz/LeetCodeRating/english/version.json" +
        "?timeStamp=" +
        new Date().getTime(),
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      onload: function (res) {
        if (res.status === 200) {
          console.log("enter home page check version once...")
          const dataStr = res.response
          const json = JSON.parse(dataStr)
          const v = json.version
          const updateContent = json.content
          if (v !== version) {
            layer.open({
              content:
                '<div style="color:#000; padding: 8px;">' +
                '<p><strong>LeetCodeRating</strong> has a new version!</p>' +
                '<p><strong>Update content:</strong></p>' +
                '<div style="background: #f5f5f5; padding: 8px; border-radius: 4px; margin: 8px 0;">' +
                updateContent +
                '</div>' +
                '</div>',
              btn: ['Install Update', 'Later'],
              yes: function (index) {
                // 打开脚本页面,让用户可以安装更新
                window.open(
                  "https://raw.githubusercontent.com/zhang-wangz/LeetCodeRating/english/leetcodeRating_greasyfork.user.js" +
                    "?timeStamp=" +
                    new Date().getTime(),
                  "_blank"
                )
                layer.close(index)
              },
              btn2: function (index) {
                layer.close(index)
              }
            })
          } else {
            console.log(
              "leetcodeRating difficulty plugin is currently the latest version~"
            )
          }
        }
      },
      onerror: function (err) {
        console.error("[Version Check] Failed to fetch version:", err)
      },
    })
  }

  // 初始处理当前页面
  console.log(`[Script Init] Starting LeetCodeRating script v${version}`)
  console.log(`[Script Init] Current URL: ${location.href}`)
  console.log(
    `[Script Init] t2rate data available: ${Object.keys(t2rate).length} entries`
  )
  tryProcess()
})()