プログラミング

LeetCode の問題リストページで Like/Dislike 数を表示するだけの雑な Chrome 拡張を作ってみた

週に数回、朝一で LeetCode の問題を解いています。 Difficulty で Easy や Medium を選択して、順番に解いていっています。 ただ、Dislike 数が Like 数以上ある問題があり、そういった問題は、問題の説明からは理解できないようなエッジケース処理を要求されたり、難易度が明らかに選択した難易度よりも高い場合が多いように感じています。そのため、そういった問題は飛ばしています。 しかし、Like/Dislike 数を見るためには問題のページに飛ぶ必要があり、また連続でそういう問題があると前はどこまでやったかわからなくなって、また飛ばした問題のページで Like/Dislike 数を見たりしていて、少し面倒に感じていました。 そこで、Chrome 拡張の勉強がてら問題のリストのページに Like/Dislike 数を表示するだけの簡単な Chrome 拡張を作成してみました。各問題の Like/Dislike 数は、問題のページに飛んだときの Like/Dislike 数をブラウザの LocalStorage に記録したものを以下のように表示します。
LocalStorage をクリアしたら見れなくなりますし、問題のページに飛んだことのある問題にしか Like/Dislike 数が表示されないなど問題が多くかなり雑ですが、上記の問題を解決するためだけの Chrome 拡張ですし、ストアで公開せずに個人的に利用するだけなので良しとします。

コードと使い方

以下の Github リポジトリにコードを置きました。
Google Chrome extension to save number of like and dislike at last visit time for each problems and show them on problem list page. - zuqqhi2/leetcode-show-numlike
zuqqhi2/leetcode-show-numlike - GitHub
インストール方法としては、以下のように Google Chrome の拡張機能の画面に進み、「パッケージ化されていない拡張機能を読み込む」ボタンを押して、Github リポジトリからダウンロードした「leetcode-show-numlike」ディレクトリを選択するだけです。
あとは、LeetCode の問題のリストページに進むと、表に like,dislike というカラムが表示されます。各問題のページに飛んでから問題のリストページに戻ると、Like/Dislike 数が表示されます。 ページを移動すると Like/Dislike 数が表示されなくなるので、「rows per page」は「all」にしてください。

解説

Chrome 拡張の作り方

今まで Chrome 拡張を作ったことがなく、今回始めて作ったのですが、思った以上に簡単でした。 最小限の構成としては、同じディレクトリに manifest.json とメインの Javascript ファイルを設置するだけです。
$ tree zuqqhi2-ext-test/
zuqqhi2-ext-test/
├── app.js
└── manifest.json
 
0 directories, 2 files 
manifest.json と app.js の中身を以下のように書いて、「パッケージ化されていない拡張機能を読み込む」から拡張機能をインストールすれば、LeetCode の問題のリストページにアクセスした際にダイアログボックスが表示されます。
 {
  "name": "Zuqqhi2ExtTest",
  "version": "0.1.0",
  "manifest_version": 2,
  "description": "Test.",
  "content_scripts": [{
    "matches": ["https://leetcode.com/problemset/all/*"],
    "js": ["app.js"],
    "css": []
   }],
   "permissions": []
 } 
alert('Test'); 
上記のサンプルも今回作ったものでも permissions には何も指定していないですが、permissions については以下のサイトにまとまっていました。
拡張機能が特別な権限を必要とする際には permission キーを使用します。このキーには文字列の配列を指定し、各文字列がパーミッションを要求します。
permissions - Mozilla | MDN - 

今回作ったもの

以下のことをやっているだけのすごく単純な作りです。
  1. 処理に使う要素が読み込まれるのを待ちます。
  2. 問題のリストページの場合は、表に列を追加して、LocalStorage から読み込んだ問題ごとの Like/Dislike 数を追加した列に表示します。
  3. 個別の問題ページの場合は、LocalStorage に Like/Dislike 数を保存します。
loadProblemInfo 関数は LocalStorage から Like/Dislike 数を取り出すだけの関数です。一応なかったりJSON.parse に失敗した場合は、初期化します。
// Common functions
function loadProblemInfo() {
  // Clean or initialize localStorage
  var problem_info = {}
  if (localStorage.getItem('zuqqhi2-leetcode-show-numlike')) {
    try {
      problem_info = JSON.parse(localStorage.getItem('zuqqhi2-leetcode-show-numlike'))
    } catch (e) {
      localStorage.removeItem('zuqqhi2-leetcode-show-numlike')
      localStorage.setItem('zuqqhi2-leetcode-show-numlike', '{}')
    }
  } else {
    localStorage.setItem('zuqqhi2-leetcode-show-numlike', '{}')
  }

  return problem_info
}
ページが表示されてからも処理に使う要素が表示されるまで待つようにしています。待たない場合は要素がなくてエラーになってしまうので。 以下のコードの部分で問題のリストページでは MutationObserver で要素が表示されるのを待っているのですが、個別の問題のページでは MutationObserver がうまく動かなかったので setInterval で適当に 500 ミリ秒ごとに要素が表示されるか見て、要素が表示されていれば LocalStorage に Like/Dislike 数を保存して終了する形になっています。 例えば個別の問題のページでは、Like/Dislike ボタンの要素を取得するのに(仕方なく)「btn__r7r7」クラスを指定しているので、個別の問題のページがちょっとでも修正されるとクラスが見つからなくて動かなくなると思います。
// Problem list page (Show number of like and dislike)
var observer = undefined
if (document.location.href.startsWith('https://leetcode.com/problemset/all/')) {
  observer = new MutationObserver(() => {
    const elems = document.querySelectorAll(".question-list-base > .question-list-table > .table > .reactable-data > tr")
    if (elems.length > 0) {
      ...
      observer.disconnect()
    }
  })

  const targetNode = document.getElementsByClassName('question-list-base')[0]
  observer.observe(
    targetNode,
    {childList: true, subtree: true}
  )
 
// Each problem page (Save number of like and dislike)
// Note: MutationOberser doesn't work on this page. So, just using setInterval function.
} else {
  observer = () => {
    const elems = document.querySelectorAll(".btn__r7r7")
    if (elems.length === 4) {
      ...
    }
  }

  const observation = () => {
    console.log('zuqqhi2-leetcode-show-numlike: under observation...')
    const targetNode = document.getElementsByClassName('btn__r7r7')[0]
    if (targetNode !== undefined) {
      console.log('zuqqhi2-leetcode-show-numlike: observation is finished')
      observer()
      return true
    } else {
      return false
    }
  }
  const interval_id = setInterval(() => {
    if (observation()) { clearInterval(interval_id); }
  }, 500)
}
zuqqhi2

某Web系の会社でエンジニアをやっています。 学術的なことに非常に興味があります。 趣味は楽器演奏、ジョギング、読書、料理などなど手広くやっています。