DGS Utilities

Improvements of dragongoserver.net: conditional moves, grey skin, keyboard shortcuts.

  1. // ==UserScript==
  2. // @name DGS Utilities
  3. // @description Improvements of dragongoserver.net: conditional moves, grey skin, keyboard shortcuts.
  4. // @author TPReal
  5. // @namespace https://gf.qytechs.cn/users/9113
  6. // @version 0.4.8
  7. // @match *://www.dragongoserver.net/*
  8. // @run-at document-start
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /* jshint ignore:start */
  13. (()=>{
  14. 'use strict';
  15.  
  16. const EXTRA_CSS=`\
  17. table.MessageForm textarea[name='message'] {
  18. height: 2.5em;
  19. }
  20.  
  21. div#condMoves {
  22. margin: 1em;
  23. }
  24.  
  25. div#condMoves input {
  26. width: 35em;
  27. margin-left: 0.5em;
  28. }
  29.  
  30. div#condMoves > * {
  31. vertical-align: baseline;
  32. }
  33.  
  34. span.game-info-char {
  35. font-size: 0.8em;
  36. }
  37.  
  38. div#loadingInfo {
  39. position: fixed;
  40. bottom: 0;
  41. left: 0;
  42. background: #0c41c9;
  43. color: #fffc70;
  44. margin: 8px;
  45. padding: 0 0.3em;
  46. display: none;
  47. }
  48.  
  49. table#PoolViewerTable td.Matrix > a[title~="[Timeout]"]::after {
  50. content: "T";
  51. font-size: 0.5em;
  52. }
  53. `;
  54.  
  55. const SKIN_GREY=`\
  56. body {
  57. background: #f8f8f8;
  58. font-family: Georgia, Times, Times New Roman, serif;
  59. }
  60.  
  61. table#pageHead, table#pageFoot {
  62. border: solid 1px #707070;
  63. background: #d0d0d0;
  64. color: #202020;
  65. margin-top: -2px;
  66. }
  67.  
  68. table#pageHead a, table#pageFoot a {
  69. color: #202020;
  70. }
  71.  
  72. table#pageMenu {
  73. background: #f8f8f8;
  74. border-color: #707070;
  75. }
  76.  
  77. span.MainMenuCount {
  78. color: #a0a0a0;
  79. }
  80.  
  81. table.GameInfos tr {
  82. background: #eeeeee;
  83. }
  84.  
  85. table.Links a {
  86. color: #202020;
  87. }
  88.  
  89. a[target] {
  90. color: initial;
  91. }
  92.  
  93. a {
  94. color: initial;
  95. }
  96.  
  97. table.Goban img.brdl, table.Goban img.brdn, table.Goban img[src='images/blank.gif'] {
  98. filter: grayscale() brightness(102%);
  99. }
  100.  
  101. td.Logo1 img[alt='Dragon'] {
  102. filter: grayscale() brightness(101%);
  103. }
  104.  
  105. img[alt='Dragon Go Server'] {
  106. filter: grayscale() brightness(68.5%) contrast(300%);
  107. }
  108.  
  109. img[alt='Samuraj Logo'] {
  110. filter: grayscale() brightness(97%);
  111. margin-top: -10px;
  112. }
  113.  
  114. img[alt='Rating graph'] {
  115. filter: grayscale();
  116. }
  117.  
  118. table.Table tr.Row2, table.Table tr.Row4 {
  119. background: #f4f4f4;
  120. }
  121.  
  122. table.Table tr.Row1 td.RemTimeWarn2, table.Table tr.Row2 td.RemTimeWarn2,
  123. table.Table tr.Row3 td.RemTimeWarn2, table.Table tr.Row4 td.RemTimeWarn2 {
  124. background: initial;
  125. }
  126.  
  127. table.Table tr.Row1 td.RemTimeWarn1, table.Table tr.Row2 td.RemTimeWarn1,
  128. table.Table tr.Row3 td.RemTimeWarn1, table.Table tr.Row4 td.RemTimeWarn1 {
  129. background: #fa96a6;
  130. }
  131.  
  132. h3.Header, h3.Header .Rating, h3.Header .User {
  133. color: initial;
  134. }
  135.  
  136. table.Infos, table.InfoBox, table.MessageBox {
  137. border-collapse: collapse;
  138. border-color: #707070;
  139. }
  140.  
  141. table.Infos td, table.InfoBox td, table.MessageBox td, table.Infos th, table.Infos td {
  142. border-color: #707070;
  143. }
  144.  
  145. img[alt='RSS'] {
  146. display: none;
  147. }
  148.  
  149. font[color] {
  150. color: initial;
  151. }
  152.  
  153. table.Table td.Sgf a {
  154. color: initial;
  155. }
  156.  
  157. td.ServerHome {
  158. color: rgba(0,0,0,0);
  159. }
  160.  
  161. td.ServerHome select, td.ServerHome input {
  162. display: none;
  163. }
  164.  
  165. table#PoolViewerTable tr.Empty, table#PoolViewerTable tr.Title {
  166. background: initial;
  167. }
  168.  
  169. dl.ExtraInfos dd.Guidance, dl.ExtraInfos dd.Info {
  170. color: initial;
  171. }
  172.  
  173. table.Infos th {
  174. color: initial;
  175. }
  176.  
  177. table.GameNotes th {
  178. background: #d0d0d0;
  179. color: initial;
  180. }
  181.  
  182. div#loadingInfo {
  183. background: #d0d0d0;
  184. color: #202020;
  185. border: solid 1px #707070;
  186. }
  187. `;
  188.  
  189. const RELOAD_INTERVAL=10*60*1000;
  190.  
  191. class LoadingIndicator{
  192.  
  193. constructor(){
  194. this.counter_=0;
  195. this.element_=null;
  196. }
  197.  
  198. static create(){
  199. return new LoadingIndicator();
  200. }
  201.  
  202. init(){
  203. this.element_=document.createElement("div");
  204. this.element_.id="loadingInfo";
  205. this.element_.title="Working...";
  206. this.element_.innerHTML="...";
  207. document.body.appendChild(this.element_);
  208. this.setVisibility_();
  209. }
  210.  
  211. registerPromise(loadingPromise){
  212. this.counter_++;
  213. this.setVisibility_();
  214. loadingPromise.catch(()=>null).then(()=>{
  215. setTimeout(()=>{
  216. this.counter_--;
  217. this.setVisibility_();
  218. },10);
  219. });
  220. return loadingPromise;
  221. }
  222.  
  223. registerStart(){
  224. let done;
  225. this.registerPromise(new Promise(success=>{
  226. done=()=>success(null);
  227. }));
  228. return done;
  229. }
  230.  
  231. setVisibility_(){
  232. if(this.element_)
  233. this.element_.style.display=this.counter_?"block":null;
  234. }
  235.  
  236. }
  237.  
  238. const LOADING_INDICATOR=LoadingIndicator.create();
  239.  
  240. async function ajax(path,params={},init={}){
  241. const searchParams=new URLSearchParams();
  242. for(const param of Object.keys(params))
  243. searchParams.set(param,params[param]);
  244. const paramsStr=searchParams.toString();
  245. const done=LOADING_INDICATOR.registerStart();
  246. try{
  247. const response=await fetch(
  248. `${path}${paramsStr?`?${paramsStr}`:``}`,
  249. Object.assign({credentials:"include"},init));
  250. return response.ok?response.text():Promise.reject(response);
  251. }finally{
  252. done();
  253. }
  254. }
  255.  
  256. class ParseError extends Error{}
  257.  
  258. const coord={
  259.  
  260. SGFPattern:"[a-s]{2}",
  261.  
  262. fromSGF(str){
  263. if(!str.match(/^[a-s]{2}$/))
  264. throw new Error(`Bad SGF coordinates format: ${str}`);
  265. const x=str.charCodeAt(0)-96;
  266. const y=str.charCodeAt(1)-96;
  267. return `${String.fromCharCode((x>=9?x+1:x)+96)}${20-y}`;
  268. },
  269.  
  270. validate(str){
  271. if(!str.match(/^[a-hj-t]([1-9]|1[0-9])$/))
  272. throw new ParseError(`Bad move coordinates: ${str||"(empty)"}`);
  273. },
  274.  
  275. toSGF(str){
  276. this.validate(str);
  277. let xc=str.charCodeAt(0)-96;
  278. const x=xc>=9?xc-1:xc;
  279. const y=20-parseInt(str.substr(1),10);
  280. return String.fromCharCode(x+96,y+96);
  281. },
  282.  
  283. };
  284.  
  285. class CondMoves{
  286.  
  287. constructor(gameId,atMoveNo,tree={}){
  288. this.gameId=gameId;
  289. this.atMoveNo=atMoveNo;
  290. this.tree=tree;
  291. }
  292.  
  293. static parseUserString(gameId,atMoveNo,str){
  294. const condMoves=new CondMoves(gameId,atMoveNo);
  295. for(const path of str.trim().split(/\s*[,;\n]\s*/)){
  296. if(!path)
  297. continue;
  298. const moves=path.split(/\s+/);
  299. for(const move of moves)
  300. coord.validate(move);
  301. if(moves.length%2!==0)
  302. throw new ParseError(`Path with odd length (no response specified for opponent's last move): ${moves.join(" ")}`);
  303. let cm=condMoves;
  304. for(let i=0;i<moves.length;i+=2){
  305. const branch=cm.getBranch_(moves[i],moves[i+1]);
  306. cm=branch.condMoves;
  307. }
  308. }
  309. return condMoves;
  310. }
  311.  
  312. getBranch_(move,response){
  313. let branch=this.tree[move];
  314. if(branch&&branch.response!==response)
  315. throw new ParseError(`Inconsistent response to ${move} in different branches: ${branch.response} vs ${response}`);
  316. if(!branch){
  317. branch={response,condMoves:new CondMoves(this.gameId,this.atMoveNo+2)};
  318. this.tree[move]=branch;
  319. }
  320. return branch;
  321. }
  322.  
  323. hasMoves(){
  324. return !!Object.keys(this.tree).length;
  325. }
  326.  
  327. toUserString(){
  328. return this.toUserStringHelper_().map(path=>path.join(" ")).join(", ");
  329. }
  330.  
  331. toUserStringHelper_(){
  332. const paths=[];
  333. if(this.hasMoves()){
  334. for(const move of Object.keys(this.tree)){
  335. const branch=this.tree[move];
  336. for(const path of branch.condMoves.toUserStringHelper_())
  337. paths.push([move,branch.response,...path]);
  338. }
  339. }else
  340. paths.push([]);
  341. return paths;
  342. }
  343.  
  344. toString(){
  345. return `CondMoves[${this.gameId}@${this.atMoveNo}: ${this.toUserString()}]`;
  346. }
  347.  
  348. serialise(){
  349. return `@${this.atMoveNo} ${this.toUserString()}`;
  350. }
  351.  
  352. static deserialise(gameId,str){
  353. const mat=str.match(/^\s*@(\d+) (.*)$/);
  354. if(!mat)
  355. throw new Error(`Bad serialised conditional moves: ${str}`);
  356. return CondMoves.parseUserString(gameId,parseInt(mat[1],10),mat[2]);
  357. }
  358.  
  359. }
  360.  
  361. /*
  362. class Storage{
  363.  
  364. constructor(base,fieldsWithDefaults,serialiser=JSON){
  365. const serialise=(serialiser.serialise||serialiser.stringify).bind(serialiser);
  366. for(const field of Object.keys(fieldsWithDefaults)){
  367. const defVal=fieldsWithDefaults[field];
  368. Object.defineProperty(this,field,{
  369. get:()=>{
  370. const str=base.getItem(field);
  371. if(str===null)
  372. return defVal;
  373. return serialiser.parse(str);
  374. },
  375. set:v=>{
  376. base.setItem(field,serialise(v));
  377. },
  378. });
  379. }
  380. }
  381.  
  382. }
  383.  
  384. const LOCAL_STORAGE=new Storage(localStorage,{});
  385. */
  386.  
  387. class PrivateNotes{
  388.  
  389. constructor(gameId,base,condMoves){
  390. this.gameId=gameId;
  391. this.base=base;
  392. this.condMoves=condMoves;
  393. }
  394.  
  395. static parse(gameId,text){
  396. const mat=text.match(/^([\s\S]*?)(?:(?:^|\n)Conditional moves: (.+)\n*)?$/);
  397. let base=mat[1];
  398. let condMoves=null;
  399. if(mat[2])
  400. try{
  401. condMoves=CondMoves.deserialise(gameId,mat[2]);
  402. if(!(condMoves instanceof CondMoves))
  403. throw new Error(`Expected CondMoves, got ${condMoves}`);
  404. }catch(e){
  405. console.warn(`Cannot parse conditional moves from private notes: ${mat[2]}, error: ${e}`);
  406. base=mat[0];
  407. condMoves=null;
  408. }
  409. base=base.trim();
  410. if(base)
  411. base+="\n";
  412. return new PrivateNotes(gameId,base,condMoves);
  413. }
  414.  
  415. static empty(gameId){
  416. return new PrivateNotes(gameId,"",null);
  417. }
  418.  
  419. async saveCondMoves(condMoves){
  420. this.condMoves=condMoves;
  421. let text=this.base;
  422. if(condMoves&&condMoves.hasMoves())
  423. text+=`\n\n\nConditional moves: ${condMoves.serialise()}\n`;
  424. await ajax("/quick_do.php",{
  425. obj:"game",
  426. cmd:"save_notes",
  427. gid:this.gameId,
  428. notes:text,
  429. });
  430. ajax("/quick_do.php",{
  431. obj:"game",
  432. cmd:"hide_notes",
  433. gid:this.gameId,
  434. });
  435. }
  436.  
  437. toString(){
  438. return `PrivateNotes[condMoves=${this.condMoves}]`;
  439. }
  440.  
  441. }
  442.  
  443. class GameSGF{
  444.  
  445. constructor(gameId,moveNo,lastMove,privateNotes,hasExtraComments){
  446. this.gameId=gameId;
  447. this.moveNo=moveNo;
  448. this.lastMove=lastMove;
  449. this.privateNotes=privateNotes;
  450. this.hasExtraComments=hasExtraComments;
  451. }
  452.  
  453. get condMoves(){
  454. return this.privateNotes&&this.privateNotes.condMoves;
  455. }
  456.  
  457. static parse(gameId,sgf){
  458. const nodes=sgf.split("\n;").slice(1);
  459. const firstNodeTags=GameSGF.parseTags_(nodes[0]);
  460. const moveNo=firstNodeTags.XM?parseInt(firstNodeTags.XM,10):null;
  461. const lastNodeTags=GameSGF.parseTags_(nodes[nodes.length-1]);
  462. const lastMoveSGF=lastNodeTags.B||lastNodeTags.W;
  463. const lastMove=lastMoveSGF?coord.fromSGF(lastMoveSGF):null;
  464. const lastMoveComment=lastNodeTags.C;
  465. let privateNotes=null;
  466. let hasExtraComments=false;
  467. if(lastMoveComment){
  468. const nMat=lastMoveComment.match(/(?:^|\n)Notes - .+? \(.+?\):\n([\s\S]*)/);
  469. if(nMat){
  470. privateNotes=PrivateNotes.parse(gameId,nMat[1]);
  471. hasExtraComments=nMat[0].trim()!==lastMoveComment.trim();
  472. }else
  473. hasExtraComments=true;
  474. }
  475. return new GameSGF(gameId,moveNo,lastMove,privateNotes,hasExtraComments);
  476. }
  477.  
  478. static async load(gameId,allowCache=false){
  479. return GameSGF.parse(gameId,await ajax("/sgf.php",{
  480. gid:gameId,
  481. owned_comments:1,
  482. quick_mode:1,
  483. no_cache:allowCache?0:1,
  484. }));
  485. }
  486.  
  487. static parseTags_(node){
  488. const regexp=/([A-Z]{1,2})\[/g;
  489. const result={};
  490. for(;;){
  491. const mat=regexp.exec(node);
  492. if(!mat)
  493. break;
  494. let value="";
  495. let i=regexp.lastIndex;
  496. for(;;){
  497. if(i>=node.length)
  498. break;
  499. if(node[i]==="]"&&!(i+1<node.length&&node[i+1]==="["))
  500. break;
  501. if(node[i]==="\\"&&i+1<node.length&&(node[i+1]==="\\"||node[i+1]==="["))
  502. value+=node[++i];
  503. else
  504. value+=node[i];
  505. i++;
  506. }
  507. result[mat[1]]=value;
  508. regexp.lastIndex=i;
  509. }
  510. return result;
  511. }
  512.  
  513. async executeCondMoves(){
  514. if(!this.condMoves||!this.moveNo||!this.lastMove)
  515. return {};
  516. const {clearCondMoves,condMoves:newCondMoves,response}=this.analyseCondMoves_();
  517. let condMovesToSave=clearCondMoves?null:newCondMoves;
  518. let promise;
  519. if(response){
  520. console.debug(`Responding in game ${this.gameId} to ${this.lastMove} with ${response}`);
  521. promise=ajax("/quick_do.php",{
  522. obj:"game",
  523. cmd:"move",
  524. gid:this.gameId,
  525. move_id:this.moveNo,
  526. move:coord.toSGF(response),
  527. },{method:"POST"}).then(()=>({responded:true}));
  528. promise.catch(error=>{
  529. console.warn(`Responding in game ${this.gameId} at move ${this.moveNo} with ${response} failed: ${error}`);
  530. condMovesToSave=null;
  531. });
  532. }else
  533. promise=Promise.resolve({});
  534. promise.catch(()=>null).then(()=>this.saveCondMoves(condMovesToSave));
  535. return promise;
  536. }
  537.  
  538. analyseCondMoves_(){
  539. if(this.hasExtraComments){
  540. console.debug(`In game ${this.gameId} additional information is associated with last move; clearing conditional moves`);
  541. return {clearCondMoves:true};
  542. }
  543. if(this.moveNo!==this.condMoves.atMoveNo){
  544. console.debug(`In game ${this.gameId} current move is ${this.moveNo}, but conditional moves defined for move ${this.condMoves.atMoveNo}; clearing conditional moves`);
  545. return {clearCondMoves:true};
  546. }
  547. const branch=this.condMoves.tree[this.lastMove];
  548. if(!branch){
  549. console.debug(`In game ${this.gameId} no response defined for ${this.lastMove}; clearing conditional moves`);
  550. return {clearCondMoves:true};
  551. }
  552. return branch;
  553. }
  554.  
  555. async checkCondMovesOnOpponentTurn(){
  556. if(!this.condMoves||!this.moveNo)
  557. return null;
  558. if(this.moveNo>this.condMoves.atMoveNo){
  559. console.debug(`In game ${this.gameId} current move is ${this.moveNo}, but conditional moves defined for move ${this.condMoves.atMoveNo}; clearing conditional moves`);
  560. return this.saveCondMoves(null);
  561. }
  562. return null;
  563. }
  564.  
  565. async saveCondMoves(condMoves){
  566. if(!this.privateNotes)
  567. this.privateNotes=PrivateNotes.empty(this.gameId);
  568. return this.privateNotes.saveCondMoves(condMoves);
  569. }
  570.  
  571. toString(){
  572. return `GameSGF[gameId=${this.gameId}, @${this.moveNo}, lastMove=${this.lastMove
  573. }, privateNotes=${this.privateNotes}, hasExtraComments=${this.hasExtraComments}]`;
  574. }
  575.  
  576. }
  577.  
  578. class QuickStatus{
  579.  
  580. constructor(objects){
  581. this.objects_=objects;
  582. }
  583.  
  584. static parse(status){
  585. const errMat=status.match(/\[#Error: (.+?)\]$/m);
  586. if(errMat)
  587. throw new Error(`QuickStatus parse error: ${errMat[1]}`);
  588. const objectsByType=new Map();
  589. const headersByType=new Map();
  590. const getFields=str=>{
  591. const fields=[];
  592. let field="";
  593. let quoted=false;
  594. for(let i=0;i<str.length;i++){
  595. if(str[i]==="'"){
  596. if(quoted)
  597. quoted=false;
  598. else if(field)
  599. throw new Error(`Unexpected quote in string: ${str}`);
  600. else
  601. quoted=true;
  602. }else if(str[i]==="\\"&&quoted){
  603. if(++i>=str.length)
  604. throw new Error(`Unexpected end of string: ${str}`);
  605. field+=str[i];
  606. }else if(str[i]===","&&!quoted){
  607. fields.push(field);
  608. field="";
  609. }else
  610. field+=str[i];
  611. }
  612. fields.push(field);
  613. return fields;
  614. };
  615. const scan=function*(str,regexp){
  616. let mat;
  617. while(mat=regexp.exec(str))
  618. yield mat;
  619. };
  620. for(const mat of scan(status,/^## ([A-Z]),(.+?)$/mg))
  621. headersByType.set(mat[1],getFields(mat[2]));
  622. for(const mat of scan(status,/^([A-Z]),(.+?)$/mg)){
  623. const type=mat[1];
  624. let objects=objectsByType.get(type);
  625. if(!objects){
  626. objects=[];
  627. objectsByType.set(type,objects);
  628. }
  629. const headers=headersByType.get(type);
  630. const fields=getFields(mat[2]);
  631. const object={};
  632. objects.push(object);
  633. for(let i=0;i<headers.length;i++)
  634. object[headers[i]]=fields[i];
  635. }
  636. return new QuickStatus(objectsByType);
  637. }
  638.  
  639. static async load(){
  640. return QuickStatus.parse(await ajax("/quick_status.php",{version:2,no_cache:1,order:0}));
  641. }
  642.  
  643. get messages(){
  644. return this.objects_.get("M")||[];
  645. }
  646.  
  647. get games(){
  648. return this.objects_.get("G")||[];
  649. }
  650.  
  651. }
  652.  
  653. class TitleUpdater{
  654.  
  655. constructor(base,initialCount){
  656. this.base_=base;
  657. this.initialCount_=initialCount;
  658. }
  659.  
  660. static create(){
  661. const mat=document.title.match(/^(.+?)(?: \((\d+)\))?$/)
  662. return new TitleUpdater(mat[1],mat[2]||null);
  663. }
  664.  
  665. quickUpdate(){
  666. this.updateInternal_(this.initialCount_,0);
  667. }
  668.  
  669. update(quickStatus){
  670. this.updateInternal_(quickStatus.games.length,quickStatus.messages.length);
  671. }
  672.  
  673. updateInternal_(gamesCount,messagesCount){
  674. document.title=`${gamesCount==null?``:`[${gamesCount}${messagesCount?`, ${messagesCount}`:``}] `}${this.base_}`;
  675. }
  676.  
  677. }
  678.  
  679. class Manager{
  680.  
  681. constructor(titleUpdater){
  682. this.titleUpdater_=titleUpdater;
  683. this.quickStatus=null;
  684. }
  685.  
  686. static start(){
  687. const titleUpdater=TitleUpdater.create();
  688. titleUpdater.quickUpdate();
  689. const manager=new Manager(titleUpdater);
  690. const update=async()=>{
  691. try{
  692. await manager.update();
  693. }finally{
  694. scheduleUpdate();
  695. }
  696. };
  697. const scheduleUpdate=()=>setTimeout(update,RELOAD_INTERVAL);
  698. scheduleUpdate();
  699. return manager;
  700. }
  701.  
  702. async update(){
  703. if(location.pathname==="/index.php")
  704. return;
  705. if(location.pathname==="/status.php")
  706. location.reload();
  707. else{
  708. this.quickStatus=await QuickStatus.load();
  709. this.titleUpdater_.update(this.quickStatus);
  710. Promise.all(this.quickStatus.games.map(game=>
  711. GameSGF.load(game.game_id).then(sgf=>sgf.executeCondMoves()).catch(error=>{
  712. console.warn(`Error while executing conditional move for game ${gameId}: ${error}`);
  713. return {error};
  714. }))).then(results=>{
  715. if(results.some(result=>result.responded))
  716. this.update();
  717. });
  718. }
  719. }
  720.  
  721. }
  722.  
  723. function addCSS(css){
  724. const styleElem=document.createElement("style");
  725. styleElem.innerText=css;
  726. document.head.appendChild(styleElem);
  727. }
  728.  
  729. async function start(){
  730.  
  731. const COND_MOVES_TR_INNER_HTML=`\
  732. <td class="UnderBoard">
  733. <div id="condMoves" title="Specify branches of conditional moves, e.g.: f3 c6 d2 c3, c6 f3
  734. Right-click on board to enter coordinates">
  735. <span>Conditional moves:</span>
  736. <input id="condMoves" type="text">
  737. <button id="saveCondMoves" type="button">Save</button>
  738. </div>
  739. </td>`;
  740.  
  741. LOADING_INDICATOR.init();
  742.  
  743. const manager=Manager.start();
  744.  
  745. const keyHandlers=new Map();
  746.  
  747. if(location.pathname==="/status.php"){
  748. Promise.all([...document.querySelectorAll("table#gameTable tr td.Button:first-child")].map(gameIdElem=>{
  749. const gameId=gameIdElem.innerText.trim();
  750. return GameSGF.load(gameId).then(sgf=>sgf.executeCondMoves()).catch(error=>{
  751. console.warn(`Error while executing conditional move for game ${gameId}: ${error}`);
  752. return {error};
  753. });
  754. })).then(results=>{
  755. if(results.some(result=>result.responded))
  756. location.reload();
  757. });
  758. }else if(location.pathname==="/game.php"){
  759. const gameId=location.searchParams.get("g")||location.searchParams.get("gid");
  760. let gameState;
  761. if(location.searchParams.get("a")==="domove")
  762. gameState="confirmMove";
  763. else if(location.searchParams.get("a")==="resign")
  764. gameState="resigning";
  765. else if(document.querySelector("dl.ExtraInfos dd.Score"))
  766. gameState="finished";
  767. else if(document.querySelector("input[name='action'][value='choose_move']"))
  768. gameState="myMove";
  769. else
  770. gameState="theirMove";
  771.  
  772. const eidogoLinkImg=document.querySelector("a.NoPrint > img[title='EidoGo Game Player']");
  773. if(eidogoLinkImg)
  774. eidogoLinkImg.parentElement.setAttribute("target","_blank");
  775.  
  776. if(gameState==="confirmMove"){
  777. const linkifyField=(imageElement,href)=>{
  778. if(!imageElement)
  779. return;
  780. const td=imageElement.parentElement;
  781. const link=document.createElement("a");
  782. link.setAttribute("href",href);
  783. link.appendChild(imageElement);
  784. td.appendChild(link);
  785. };
  786. const moveParams=new URLSearchParams(location.searchParams);
  787. for(const mark of [".",","]){
  788. for(const img of document.querySelectorAll(`table#Goban td[id].brdx img[alt='${mark}'].brdx`)){
  789. moveParams.set("c",img.parentElement.id);
  790. linkifyField(img,`/game.php?${moveParams}`);
  791. }
  792. }
  793. moveParams.delete("a");
  794. moveParams.delete("c");
  795. for(const mark of ["@","#"])
  796. linkifyField(document.querySelector(`table#Goban td[id].brdx img[alt='${mark}'].brdx`),`/game.php?${moveParams}`);
  797.  
  798. const cancelMove=()=>document.querySelector("input[name='cancel']").click();
  799. keyHandlers.set("ArrowLeft",cancelMove);
  800. keyHandlers.set("Home",cancelMove);
  801. keyHandlers.set("End",cancelMove);
  802. }else{
  803. const navigate=(selOptFunc)=>{
  804. const selMoveOption=document.querySelector("select[name='gotomove'] option[selected]");
  805. if(!selMoveOption)
  806. return;
  807. const newMoveOption=selOptFunc(selMoveOption);
  808. if(newMoveOption&&newMoveOption!==selMoveOption){
  809. selMoveOption.removeAttribute("selected");
  810. newMoveOption.setAttribute("selected","");
  811. document.querySelector("input[name='movechange']").click();
  812. }
  813. };
  814. keyHandlers.set("ArrowLeft",()=>navigate(o=>o.nextElementSibling));
  815. keyHandlers.set("ArrowRight",()=>navigate(o=>o.previousElementSibling));
  816. keyHandlers.set("Home",()=>navigate(o=>o.parentElement.lastElementChild));
  817. keyHandlers.set("End",()=>navigate(o=>o.parentElement.firstElementChild));
  818. }
  819.  
  820. if(gameState==="confirmMove"||(gameState==="theirMove"&&
  821. document.querySelector("select[name='gotomove'] option[selected]:first-of-type"))){
  822. const condMovesRow=document.createElement("tr");
  823. condMovesRow.innerHTML=COND_MOVES_TR_INNER_HTML;
  824. const summaryRow=document.querySelector("table#GamePage > tbody > tr:nth-of-type(2)");
  825. condMovesRow.querySelector("td").setAttribute("colspan",summaryRow.querySelector("td").getAttribute("colspan"));
  826. if(gameState==="confirmMove")
  827. condMovesRow.querySelector("#saveCondMoves").style.display="none";
  828. const condMovesInput=condMovesRow.querySelector("input#condMoves");
  829. summaryRow.parentElement.insertBefore(condMovesRow,summaryRow);
  830. const sgfPromise=GameSGF.load(gameId);
  831. sgfPromise.then(sgf=>{
  832. if(sgf.condMoves)
  833. condMovesInput.value=sgf.condMoves.toUserString();
  834. });
  835. const moveNoOffset=gameState==="confirmMove"?2:1;
  836. const saveCondMoves=()=>{
  837. const promise=sgfPromise.then(sgf=>{
  838. const condMoves=CondMoves.parseUserString(gameId,sgf.moveNo+moveNoOffset,condMovesInput.value);
  839. condMovesInput.value=condMoves.toUserString();
  840. return sgf.saveCondMoves(condMoves);
  841. });
  842. promise.catch(error=>{
  843. let msg;
  844. if(error instanceof ParseError)
  845. msg=`Invalid conditional moves: ${error.message}`;
  846. else
  847. msg=`Error saving conditional moves: ${error}`;
  848. console.warn(msg);
  849. alert(msg);
  850. });
  851. return promise;
  852. };
  853. condMovesRow.querySelector("#saveCondMoves").addEventListener("click",()=>{
  854. saveCondMoves();
  855. return true;
  856. });
  857. condMovesInput.addEventListener("keydown",event=>{
  858. if(event.code==="Enter"){
  859. saveCondMoves();
  860. event.preventDefault();
  861. }
  862. });
  863. const rightClickHandler=event=>{
  864. if(event.button===2){
  865. const move=coord.fromSGF(event.currentTarget.id);
  866. const text=condMovesInput.value;
  867. const focused=document.activeElement===condMovesInput;
  868. let selRange;
  869. if(focused)
  870. selRange=[text.length,text.length];
  871. else
  872. selRange=[condMovesInput.selectionStart,condMovesInput.selectionEnd];
  873. const preText=text.substring(0,selRange[0]);
  874. const postText=text.substring(selRange[1]);
  875. let newText=preText;
  876. if(preText.match(/\S$/))
  877. newText+=" ";
  878. newText+=move;
  879. const newCursorPos=newText.length;
  880. if(postText.match(/^[^,\s]/))
  881. newText+=" ";
  882. newText+=postText;
  883. condMovesInput.value=newText;
  884. condMovesInput.focus();
  885. condMovesInput.setSelectionRange(newCursorPos,newCursorPos);
  886. event.preventDefault();
  887. }
  888. };
  889. for(const field of document.querySelectorAll("table#Goban td[id].brdx")){
  890. field.addEventListener("mouseup",rightClickHandler);
  891. field.addEventListener("contextmenu",event=>event.preventDefault());
  892. }
  893. let allowedSubmitButton=null;
  894. const submitHandler=event=>{
  895. if(event.target===allowedSubmitButton)
  896. return;
  897. saveCondMoves().then(()=>{
  898. allowedSubmitButton=event.target;
  899. event.target.click();
  900. });
  901. event.preventDefault();
  902. };
  903. for(const name of ["nextgame","nextstatus"]){
  904. const button=document.querySelector(`input[type='submit'][name='${name}']`);
  905. if(button)
  906. button.addEventListener("click",submitHandler);
  907. }
  908. }
  909.  
  910. keyHandlers.set("Space",()=>{
  911. const skipParams=new URLSearchParams();
  912. skipParams.set("gid",gameId);
  913. skipParams.set("nextskip","t");
  914. location.href=`/confirm.php?${skipParams}`;
  915. });
  916. }else if(location.pathname==="/show_games.php"){
  917. for(const gameIdElem of document.querySelectorAll("table#runningTable tr td.Button:first-child")){
  918. const infoElem=gameIdElem.parentElement.querySelector("td.ImagesLeft");
  919. if(infoElem){
  920. const gameId=gameIdElem.innerText.trim();
  921. GameSGF.load(gameId).then(sgf=>{
  922. return sgf.checkCondMovesOnOpponentTurn().then(()=>{
  923. if(sgf.privateNotes&&sgf.privateNotes.base.trim())
  924. infoElem.innerHTML+=` <span class="game-info-char" title="There are private notes saved for this game">(p)</span>`;
  925. if(sgf.condMoves)
  926. infoElem.innerHTML+=` <span class="game-info-char" title="Conditional moves are defined for this game">(c)</span>`;
  927. });
  928. });
  929. }
  930. }
  931. }else if(location.pathname==="/error.php")
  932. setTimeout(()=>location.href="/status.php",3600*1000);
  933. keyHandlers.set("Escape",()=>location.href=`/status.php`);
  934.  
  935. if(keyHandlers.size){
  936. document.addEventListener("keydown",event=>{
  937. if(["INPUT","TEXTAREA"].includes(event.target.tagName))
  938. return;
  939. const code=event.code;
  940. const handler=keyHandlers.get(code);
  941. if(handler){
  942. event.preventDefault();
  943. handler();
  944. }
  945. });
  946. }
  947.  
  948. }
  949.  
  950. async function onLoaded(){
  951. addCSS(EXTRA_CSS);
  952. addCSS(SKIN_GREY);
  953. location.searchParams=new URLSearchParams(location.search);
  954. try{
  955. await start();
  956. }catch(e){
  957. console.error(e);
  958. }
  959. }
  960.  
  961. if(document.readyState==="loading")
  962. document.addEventListener("DOMContentLoaded",onLoaded,false);
  963. else
  964. onLoaded();
  965.  
  966. })();

QingJ © 2025

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