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

// ==UserScript==
// @name         LeetCodeRating|English
// @namespace    https://github.com/zhang-wangz
// @version      2.0.0
// @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
// @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
// ==/UserScript==

(function () {
  "use strict"
  let t2rate = {}
  const version = "2.0.0"
  const DEBUG_MODE = false
  
  const originalConsoleLog = console.log
  if (!DEBUG_MODE) {
    try {
      console.log = function(...args) {
      }
    } catch (e) {
      window.console = Object.assign({}, console, {
        log: function(...args) {
        }
      })
    }
  }

  // a timer manager for all pages
  const TimerManager = {
    timers: {
      allProblems: null, // 题目列表页 contains all the problems
      problem: null, // 单题页 the specific problem page
      problemList: null, // 题单页 the problem list page
    },

    // 设置定时器 set timer
    set(type, intervalId) {
      this.clear(type)
      this.timers[type] = intervalId
      console.log(`[TimerManager] Set timer for ${type}: ${intervalId}`)
    },

    // 清除指定类型的定时器 clear the timer for the specific type
    clear(type) {
      if (this.timers[type]) {
        clearInterval(this.timers[type])
        console.log(
          `[TimerManager] Cleared timer for ${type}: ${this.timers[type]}`
        )
        this.timers[type] = null
      }
    },

    // 清除所有定时器 clear all timers
    clearAll() {
      Object.keys(this.timers).forEach((type) => {
        this.clear(type)
      })
      console.log("[TimerManager] Cleared all timers")
    },

    // 获取定时器ID get the timer id
    get(type) {
      return this.timers[type]
    },
  }
  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"))

  // 深拷贝 deep clone
  function deepclone(obj) {
    const str = JSON.stringify(obj)
    return JSON.parse(str)
  }

  // URL变化监听管理器 (已注释掉,改用纯定时器方式)
  /*
  const UrlChangeManager = {
    isInitialized: false,
    urlChangeHandler: null,
    
    // 初始化URL变化监听
    init() {
      if (this.isInitialized) return
      this.isInitialized = true
      
      const oldPushState = history.pushState
      const oldReplaceState = history.replaceState
      
      history.pushState = function pushState(...args) {
        const res = oldPushState.apply(this, args)
        window.dispatchEvent(new Event("urlchange"))
        return res
      }
      
      history.replaceState = function replaceState(...args) {
        const res = oldReplaceState.apply(this, args)
        window.dispatchEvent(new Event("urlchange"))
        return res
      }
      
      window.addEventListener("popstate", () => {
        window.dispatchEvent(new Event("urlchange"))
      })
      
      console.log("[UrlChangeManager] URL change detection initialized")
    },
    
    // 设置URL变化处理器(确保只有一个)
    setHandler(handler) {
      // 移除旧的处理器
      if (this.urlChangeHandler) {
        window.removeEventListener("urlchange", this.urlChangeHandler)
        console.log("[UrlChangeManager] Removed old urlchange handler")
      }
      
      // 设置新的处理器
      this.urlChangeHandler = handler
      window.addEventListener("urlchange", this.urlChangeHandler)
      console.log("[UrlChangeManager] Set new urlchange handler")
    },
    
    // 清理处理器
    clearHandler() {
      if (this.urlChangeHandler) {
        window.removeEventListener("urlchange", this.urlChangeHandler)
        this.urlChangeHandler = null
        console.log("[UrlChangeManager] Cleared urlchange handler")
      }
    }
  }

  // 监听URL变化事件(保持向后兼容)
  function initUrlChange() {
    return () => UrlChangeManager.init()
  }
  */

  // 获取时间
  function getCurrentDate(format) {
    const now = new Date()
    const year = now.getFullYear() //得到年份
    let month = now.getMonth() //得到月份
    let date = now.getDate() //得到日期
    let hour = now.getHours() //得到小时
    let minu = now.getMinutes() //得到分钟
    let sec = now.getSeconds() //得到秒
    month = month + 1
    if (month < 10) month = "0" + month
    if (date < 10) date = "0" + date
    if (hour < 10) hour = "0" + hour
    if (minu < 10) minu = "0" + minu
    if (sec < 10) sec = "0" + sec
    let time = ""
    // 精确到天
    if (format == 1) {
      time = year + "年" + month + "月" + date + "日"
    }
    // 精确到分
    else if (format == 2) {
      time =
        year + "-" + month + "-" + date + " " + hour + ":" + minu + ":" + sec
    }
    return time
  }

  function getProblemIndex(problem) {
    // we can't use problem.id because for some problems, the id here is not the problem index, so we have to extract problem index from title text
    const titleElement = problem.querySelector(".text-body .ellipsis")
    if (!titleElement) return null
    const titleText = titleElement.textContent || titleElement.innerText
    const match = titleText.match(/^(\d+)\.\s/)
    if (!match) return null
    return match[1]
  }

  let lastProcessedListContent
  let lastProcessedProblemId
  // let lastProcessedUrl = ""  // URL变化检测相关
  // let urlChangeTimeout = null  // URL变化检测相关
  function getAllProblemsData() {
    console.log(
      "[LeetCodeRating] getAllProblemsData() polling - " +
        new Date().toLocaleTimeString()
    )
    try {
      // find the element in devtools and click "copy JS path"
      const problemList = document.querySelector(
        "#__next > div.flex.min-h-screen.min-w-\\[360px\\].flex-col.text-label-1.dark\\:text-dark-label-1 > div.mx-auto.w-full.grow.lg\\:max-w-screen-xl.dark\\:bg-dark-layer-bg.lc-dsw-xl\\:max-w-none.flex.bg-white.p-0.md\\:max-w-none.md\\:p-0 > div > div.flex.w-full.flex-1.overflow-hidden > div > div.flex.flex-1.justify-center.overflow-hidden > div > div.mt-4.flex.flex-col.items-center.gap-4 > div.w-full.flex-1 > div"
      )
      // pb页面加载时直接返回
      if (problemList == undefined) {
        return
      }

      // 防止过多的无效操作
      if (
        lastProcessedListContent != undefined &&
        lastProcessedListContent == problemList.innerHTML
      ) {
        return
      }

      const problems = problemList.childNodes
      for (const problem of problems) {
        const problemIndex = getProblemIndex(problem)
        if (problemIndex == null) continue

        // get the difficulty display for the current problem
        const problemDifficulty = problem.querySelector(
          'p[class*="text-sd-easy"], p[class*="text-sd-medium"], p[class*="text-sd-hard"]'
        )
        if (problemDifficulty && t2rate[problemIndex] != undefined) {
          problemDifficulty.innerHTML = t2rate[problemIndex].Rating
        }
      }
      lastProcessedListContent = deepclone(problemList.innerHTML)
    } catch (e) {
      return
    }
  }

  function getProblemListData() {
    console.log(
      "[LeetCodeRating] getProblemListData() polling - " +
        new Date().toLocaleTimeString()
    )
    try {
      const problemList = document.querySelector(
        "#__next > div.flex.min-h-screen.min-w-\\[360px\\].flex-col.text-label-1.dark\\:text-dark-label-1 > div.mx-auto.w-full.grow.lg\\:max-w-screen-xl.dark\\:bg-dark-layer-bg.lc-dsw-xl\\:max-w-none.flex.bg-white.p-0.md\\:max-w-none.md\\:p-0 > div > div.lc-dsw-lg\\:flex-row.lc-dsw-lg\\:px-6.lc-dsw-lg\\:gap-8.lc-dsw-lg\\:justify-center.lc-dsw-xl\\:pl-10.flex.min-h-\\[600px\\].flex-1.flex-col.justify-start.px-4 > div.lc-dsw-lg\\:max-w-\\[699px\\].mt-6.flex.w-full.flex-col.items-center.gap-4 > div.lc-dsw-lg\\:max-w-\\[699px\\].w-full.flex-1 > div > div > div.absolute.left-0.top-0.h-full.w-full > div"
      )

      if (problemList == undefined) {
        return
      }

      if (
        lastProcessedListContent != undefined &&
        lastProcessedListContent == problemList.innerHTML
      ) {
        return
      }
      const problems = problemList.childNodes
      for (const problem of problems) {
        const problemIndex = getProblemIndex(problem)
        if (problemIndex == null) continue

        const problemDifficulty = problem.querySelector(
          'p[class*="text-sd-easy"], p[class*="text-sd-medium"], p[class*="text-sd-hard"]'
        )
        if (problemDifficulty && t2rate[problemIndex] != undefined) {
          problemDifficulty.innerHTML = t2rate[problemIndex].Rating
        }
      }
      lastProcessedListContent = deepclone(problemList.lastChild.innerHTML)
    } catch (e) {
      return
    }
  }

  function getProblemData() {
    console.log(
      "[LeetCodeRating] getProblemData() polling - " +
        new Date().toLocaleTimeString()
    )
    try {
      const problemTitle = document.querySelector(
        "#qd-content > div > div.flexlayout__tab > div > div > div > div > div > a"
      )

      console.log(
        "[LeetCodeRating] problemTitle:",
        problemTitle ? "Found" : "Not found"
      )
      if (problemTitle == undefined) {
        lastProcessedProblemId = "unknown"
        return
      }

      const problemIndex = problemTitle.innerText.split(".")[0].trim()

      if (
        lastProcessedProblemId != undefined &&
        lastProcessedProblemId == problemIndex
      ) {
        return
      }

      const colorSpan = document.querySelector(
        "#qd-content > div > div.flexlayout__tab > div > div > div.flex.gap-1 > div"
      )

      // 新版统计难度分数并且修改
      if (t2rate[problemIndex] != undefined) {
        console.log(
          `[LeetCodeRating] Found rating for problem ${problemIndex}: ${t2rate[problemIndex].Rating}`
        )
        colorSpan.innerHTML = t2rate[problemIndex].Rating
      } else {
        console.log(
          `[LeetCodeRating] No rating found for problem ${problemIndex}, restoring original difficulty`
        )
        // 恢复原始难度显示
        const problemDifficulty = colorSpan.getAttribute("class")
        const difficultyMap = {
          "text-difficulty-easy": "Easy",
          "text-difficulty-medium": "Medium",
          "text-difficulty-hard": "Hard",
        }

        // 检查class中包含哪种难度
        let originalDifficulty = "Unknown"
        for (const diffClass in difficultyMap) {
          if (problemDifficulty && problemDifficulty.includes(diffClass)) {
            originalDifficulty = difficultyMap[diffClass]
            break
          }
        }
        colorSpan.innerHTML = originalDifficulty
      }

      lastProcessedProblemId = deepclone(problemIndex)
    } catch (e) {
      return
    }
  }

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

  //   latestpb = JSON.parse(GM_getValue("latestpb", "{}").toString())
  preDate = GM_getValue("preDate", "")
  const now = getCurrentDate(1)

  console.log(
    `[Data Init] preDate: ${preDate}, now: ${now}, tagVersion exists: ${
      t2rate.tagVersion != undefined
    }`
  )

  if (t2rate.tagVersion == undefined || preDate == "" || 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] Successfully fetched data from server`)
          // 保留唯一标识
          t2rate = {}
          const dataStr = res.response
          const json = eval(dataStr)
          console.log(`[Data Init] Parsed ${json.length} problem records`)

          for (const element of json) {
            t2rate[element.ID] = element
            t2rate[element.ID].Rating = Number.parseInt(
              Number.parseFloat(element.Rating) + 0.5
            )
          }
          t2rate.tagVersion = {}
          console.log(
            `[Data Init] Processed t2rate, final keys count: ${
              Object.keys(t2rate).length
            }`
          )
          console.log("everyday getdate once...")
          preDate = now
          GM_setValue("preDate", preDate)
          GM_setValue("t2ratedb", JSON.stringify(t2rate))
        } else {
          console.log(`[Data Init] Failed to fetch data, status: ${res.status}`)
        }
      },
      onerror: function (err) {
        console.log("error")
        console.log(err)
      },
    })
  }

  function startTimers(url, timeout) {
    console.log(`[startTimers] Starting with URL: ${url}, timeout: ${timeout}`)

    // 清理所有定时器
    TimerManager.clearAll()

    // 根据URL匹配对应的页面类型和函数
    const pageConfig = {
      allProblems: {
        url: allProblemsUrl,
        func: getAllProblemsData,
        name: "getAllProblemsData()",
      },
      problem: {
        url: problemUrl,
        func: getProblemData,
        name: "getProblemData()",
      },
      problemList: {
        url: problemListUrl,
        func: getProblemListData,
        name: "getProblemListData()",
      },
    }

    console.log(`[startTimers] Page config:`, pageConfig)
    console.log(
      `[startTimers] URL patterns - allProblems: ${allProblemsUrl}, problem: ${problemUrl}, problemList: ${problemListUrl}`
    )

    // 找到匹配的页面类型
    let currentPageType = null
    for (const [type, config] of Object.entries(pageConfig)) {
      console.log(
        `[startTimers] Checking if ${url} starts with ${
          config.url
        }: ${url.startsWith(config.url)}`
      )
      if (url.startsWith(config.url)) {
        currentPageType = type
        console.log(`[startTimers] Matched page type: ${currentPageType}`)
        break
      }
    }

    if (!currentPageType) {
      console.log(`[startTimers] No matching page type found for URL: ${url}`)
      return
    }

    const config = pageConfig[currentPageType]
    console.log(`[startTimers] Using config for ${currentPageType}:`, config)

    // 立即执行一次
    console.log(`[startTimers] Starting immediate execution for ${config.name}`)
    config.func()

    // 启动定时器
    console.log(
      `[startTimers] Starting timer for ${currentPageType} with ${timeout}ms interval`
    )
    const timerId = setInterval(config.func, timeout)
    TimerManager.set(currentPageType, timerId)

    console.log(
      `[startTimers] Setup complete for page type: ${currentPageType}`
    )
  }

  // 原版的 clearAndStart 函数 (已注释掉,改用 startTimers)
  /*
  function clearAndStart(url, timeout, isAddEvent) {
    console.log(
      `[clearAndStart] Starting with URL: ${url}, timeout: ${timeout}`
    )

    // 清理所有定时器
    TimerManager.clearAll()

    // 根据URL匹配对应的页面类型和函数
    const pageConfig = {
      allProblems: {
        url: allProblemsUrl,
        func: getAllProblemsData,
        name: "getAllProblemsData()",
      },
      problem: {
        url: problemUrl,
        func: getProblemData,
        name: "getProblemData()",
      },
      problemList: {
        url: problemListUrl,
        func: getProblemListData,
        name: "getProblemListData()",
      },
    }

    console.log(`[clearAndStart] Page config:`, pageConfig)
    console.log(
      `[clearAndStart] URL patterns - allProblems: ${allProblemsUrl}, problem: ${problemUrl}, problemList: ${problemListUrl}`
    )

    // 找到匹配的页面类型
    let currentPageType = null
    for (const [type, config] of Object.entries(pageConfig)) {
      console.log(
        `[clearAndStart] Checking if ${url} starts with ${
          config.url
        }: ${url.startsWith(config.url)}`
      )
      if (url.startsWith(config.url)) {
        currentPageType = type
        console.log(`[clearAndStart] Matched page type: ${currentPageType}`)
        break
      }
    }

    if (!currentPageType) {
      console.log(`[clearAndStart] No matching page type found for URL: ${url}`)
    }

    if (currentPageType) {
      // 智能重试机制:立即执行,如果失败则短暂延迟后重试
      const executeWithRetry = (func, funcName, maxRetries = 3) => {
        let retryCount = 0
        const tryExecute = () => {
          console.log(
            `[LeetCodeRating] Immediate execution for URL change: ${funcName} (attempt ${
              retryCount + 1
            })`
          )

          // 记录执行前的状态
          const beforeState = {
            lastProcessedProblemId: lastProcessedProblemId,
            lastProcessedListContent: lastProcessedListContent,
          }
          func()
          const afterState = {
            lastProcessedProblemId: lastProcessedProblemId,
            lastProcessedListContent: lastProcessedListContent,
          }

          // 检查是否成功执行(状态有变化)
          const hasChanges =
            JSON.stringify(beforeState) !== JSON.stringify(afterState)

          if (!hasChanges && retryCount < maxRetries) {
            retryCount++
            console.log(
              `[LeetCodeRating] ${funcName} - No changes detected, retrying in ${
                200 * retryCount
              }ms...`
            )
            setTimeout(tryExecute, 200 * retryCount) // 递增延迟: 200ms, 400ms, 600ms
          } else if (hasChanges) {
            console.log(
              `[LeetCodeRating] ${funcName} - Successfully executed with changes`
            )
          } else {
            console.log(
              `[LeetCodeRating] ${funcName} - Max retries reached, will rely on timer`
            )
          }
        }
        tryExecute()
      }

      const config = pageConfig[currentPageType]
      console.log(
        `[clearAndStart] Using config for ${currentPageType}:`,
        config
      )

      // 立即执行
      console.log(
        `[clearAndStart] Starting immediate execution for ${config.name}`
      )
      executeWithRetry(config.func, config.name)

      // 启动定时器
      console.log(
        `[clearAndStart] Starting timer for ${currentPageType} with ${timeout}ms interval`
      )
      const timerId = setInterval(config.func, timeout)
      TimerManager.set(currentPageType, timerId)

      console.log(
        `[clearAndStart] Setup complete for page type: ${currentPageType}`
      )
    } else {
      console.log(`[clearAndStart] No page type matched, no timers started`)
    }

    // 添加URL变化监听 (已注释掉,改用纯定时器方式)
    if (isAddEvent) {
      const urlChangeHandler = () => {
        console.log("urlchange event happened")
        const newUrl = location.href
        
        // 防抖处理:如果URL没有变化,忽略此次事件
        if (newUrl === lastProcessedUrl) {
          console.log("[UrlChangeManager] URL unchanged, ignoring event")
          return
        }
        
        // 清除之前的延时器
        if (urlChangeTimeout) {
          clearTimeout(urlChangeTimeout)
        }
        
        // 延时执行,防止频繁触发
        urlChangeTimeout = setTimeout(() => {
          console.log(`[UrlChangeManager] Processing URL change: ${lastProcessedUrl} -> ${newUrl}`)
          lastProcessedUrl = newUrl
          clearAndStart(newUrl, 2000, false)
          urlChangeTimeout = null
        }, 100) // 100ms防抖延时
      }
      UrlChangeManager.setHandler(urlChangeHandler)
    }
  }
  */

  [...document.querySelectorAll("*")].forEach((item) => {
    item.oncopy = function (e) {
      e.stopPropagation()
    }
  })

  // 初始化URL变化监听 (已注释掉,改用纯定时器方式)
  // initUrlChange()()

  // 版本更新机制 (仅在主页检查)
  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 upcontent = 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;">' +
                upcontent +
                '</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.log("error")
        console.log(err)
      },
    })
  }

  // 启动主程序,使用2000ms间隔
  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`
  )

  startTimers(location.href, 2000) // 2秒间隔
})()