Greasy Fork 还支持 简体中文。

Chess Compass Analysis for Chess.com

This plugin adds buttons next to the chess board allowing for a quick post-game analysis of the current game on screen

  1. // ==UserScript==
  2. // @name Chess Compass Analysis for Chess.com
  3. // @namespace AndyVuj24
  4. // @match https://www.chess.com/*
  5. // @run-at document-end
  6. // @grant none
  7. // @version 1.3.0
  8. // @author AndyVuj24
  9. // @description This plugin adds buttons next to the chess board allowing for a quick post-game analysis of the current game on screen
  10. // @supportURL https://github.com/andyvuj24/Chess-Compass-Analysis-for-Chess.com/issues
  11. // @homepageURL https://github.com/andyvuj24/Chess-Compass-Analysis-for-Chess.com
  12. // ==/UserScript==
  13.  
  14. var counter = 0;
  15.  
  16. const $ = document.querySelector.bind(document);
  17. const $$ = document.querySelectorAll.bind(document);
  18.  
  19. const log = (message, ...data) => {
  20. if (data.length > 0) {
  21. return console.log(`[Chess.com Plugin Log]: ${message}`, data);
  22. }
  23. return console.log(`[Chess.com Plugin Log]: ${message}`);
  24. };
  25.  
  26. const isElement = (element) => {
  27. return element instanceof Element;
  28. };
  29. const isQueryString = (query) => {
  30. return (
  31. !isElement(query) && (typeof query == "string" || query instanceof String)
  32. );
  33. };
  34. const getDOMElement = (request) => {
  35. if (isElement(request)) {
  36. return request;
  37. } else if (isQueryString(request)) {
  38. return $(request);
  39. }
  40. return null;
  41. };
  42.  
  43. const addStyling = async () => {
  44. // button styling
  45. log("Adding styling to page for plugin buttons");
  46. const styleElement = document.createElement("style");
  47. styleElement.innerHTML = `.gf-chess-compass-button-container{margin:auto;display:flex;align-items:center;justify-content:center;border-radius:3px;color:#fff;font-size:16px}.gf-chess-compass-button-container>a{width:100%}.gf-chess-compass-button{background-color:#489e5d;width:100%;margin:auto;height:40px;display:flex;align-items:center;justify-content:center;border-radius:3px 3px 0 0;color:#fff;cursor:pointer;font-size:16px;font-weight:500;}.gf-chess-compass-button:hover{background-color:#57b26e}`;
  48. document.head?.appendChild?.(styleElement);
  49. };
  50.  
  51. const addButtons = async (element) => {
  52. // for button element
  53. const target = getDOMElement(element);
  54.  
  55. if (target === null) {
  56. log("Failed to add buttons to target: ", target);
  57. return;
  58. }
  59.  
  60. log("Adding buttons to sidebar");
  61. target?.insertAdjacentHTML(
  62. "beforebegin",
  63. '<div><div id="btnPGN" class="gf-chess-compass-button-container"><button class="gf-chess-compass-button">Analyze PGN with Chess Compass</button></div></div>'
  64. );
  65. target?.insertAdjacentHTML(
  66. "beforebegin",
  67. '<div><div id="btnFEN" class="gf-chess-compass-button-container"><button class="gf-chess-compass-button">Analyze FEN with Chess Compass</button></div></div>'
  68. );
  69. };
  70.  
  71. const setupButton = async (id) => {
  72. log(`Configuring button -> ${id}`);
  73. const btn = $(id);
  74. if (id.indexOf("PGN") !== -1) {
  75. btn.addEventListener("click", function () {
  76. const data =
  77. ($("chess-board")
  78. ?.game.getPGN?.()
  79. .replace(/\[[^\]]*\]|\{[^\}]*\}/g, "") ||
  80. [
  81. ...$$(
  82. "div.vertical-move-list-component span.vertical-move-list-column:not(.move-timestamps-component)"
  83. ),
  84. ]
  85. .map(({ innerText }) => innerText)
  86. .join(" ")
  87. .replace(/\[[^\]]*\]|\{[^\}]*\}/g, "")) ??
  88. null;
  89. if (!data) {
  90. log("Unable to find data for PGN");
  91. return;
  92. }
  93.  
  94. log("PGN: ", data);
  95. fetch("https://www.chesscompass.com/api/get_game_id", {
  96. method: "post",
  97. body: JSON.stringify({
  98. gameData: data,
  99. }),
  100. })
  101. .then((response) => {
  102. return response.json();
  103. })
  104. .then(({ gameId }) => {
  105. window.open(
  106. "https://www.chesscompass.com/analyze/" + gameId,
  107. "_blank"
  108. );
  109. });
  110. });
  111. }
  112.  
  113. if (id.indexOf("FEN") !== -1) {
  114. btn.addEventListener("click", function () {
  115. const data =
  116. ($("chess-board")?.game.getFEN?.() ||
  117. $("div.v-board")?.getChessboardInstance?.().state.selectedNode.fen) ??
  118. null;
  119. if (!data) {
  120. log("Unable to find data for FEN");
  121. return;
  122. }
  123. log("FEN: " + data);
  124. fetch("https://www.chesscompass.com/api/get_game_id", {
  125. method: "post",
  126. body: JSON.stringify({
  127. gameData: data,
  128. }),
  129. })
  130. .then((response) => {
  131. return response.json();
  132. })
  133. .then(({ gameId }) => {
  134. window.open(
  135. "https://www.chesscompass.com/analyze/" + gameId,
  136. "_blank"
  137. );
  138. });
  139. });
  140. }
  141. };
  142.  
  143. const waitForContainer = async () => {
  144. const selectors = [
  145. ".sidebar-component",
  146. ".sidebar-v5-component",
  147. "vertical-move-list",
  148. ];
  149. return new Promise((resolve, reject) => {
  150. for (const selector of selectors) {
  151. log("Initially trying: ", selector);
  152. const element = $(selector);
  153.  
  154. if (element) {
  155. log("Found container: ", element);
  156. resolve(main(selector));
  157. return;
  158. }
  159. }
  160.  
  161. const observer = new MutationObserver((mutations) => {
  162. mutations.forEach((mutation) => {
  163. const nodes = Array.from(mutation.addedNodes);
  164. for (const node of nodes) {
  165. for (const selector of selectors) {
  166. if (node.matches && node.matches(selector)) {
  167. log("Observer found container: ", node);
  168. observer.disconnect();
  169. resolve(main(selector));
  170. return;
  171. }
  172. }
  173. }
  174. });
  175. });
  176.  
  177. log("Made container observer");
  178. observer.observe(document.documentElement, {
  179. childList: true,
  180. subtree: true,
  181. });
  182. });
  183. };
  184.  
  185. const clearAds = async () => {
  186. log("Clearing ads...");
  187.  
  188. const adsToRemove = [
  189. "#tall-sidebar-ad",
  190. "#adblocker-check",
  191. "#board-layout-ad",
  192. ];
  193.  
  194. adsToRemove.forEach((selector) => {
  195. $$(selector).forEach((el) => {
  196. el.remove();
  197. log("Removed Ad: ", el);
  198. });
  199. });
  200.  
  201. const observer = new MutationObserver((mutations) => {
  202. mutations.forEach((mutation) => {
  203. const nodes = Array.from(mutation.addedNodes);
  204. for (const node of nodes) {
  205. for (const selector of adsToRemove) {
  206. if (node.matches && node.matches(selector)) {
  207. node.remove();
  208. }
  209. }
  210. }
  211. });
  212. });
  213.  
  214. observer.observe(document.documentElement, {
  215. childList: true,
  216. subtree: true,
  217. });
  218.  
  219. log("Ads cleared!");
  220. };
  221.  
  222. async function main(element) {
  223. await addStyling();
  224. await addButtons(element);
  225. await setupButton("#btnPGN");
  226. await setupButton("#btnFEN");
  227. await clearAds();
  228. }
  229.  
  230. if (["complete", "interactive"].indexOf(document.readyState) > -1) {
  231. waitForContainer();
  232. } else {
  233. document.addEventListener("DOMContentLoaded", waitForContainer);
  234. }