// ==UserScript==
// @name DGS Utilities
// @description Improvements of dragongoserver.net: conditional moves, grey skin, keyboard shortcuts.
// @author TPReal
// @namespace https://gf.qytechs.cn/users/9113
// @version 0.4.8
// @match *://www.dragongoserver.net/*
// @run-at document-start
// @grant none
// ==/UserScript==
/* jshint ignore:start */
(()=>{
'use strict';
const EXTRA_CSS=`\
table.MessageForm textarea[name='message'] {
height: 2.5em;
}
div#condMoves {
margin: 1em;
}
div#condMoves input {
width: 35em;
margin-left: 0.5em;
}
div#condMoves > * {
vertical-align: baseline;
}
span.game-info-char {
font-size: 0.8em;
}
div#loadingInfo {
position: fixed;
bottom: 0;
left: 0;
background: #0c41c9;
color: #fffc70;
margin: 8px;
padding: 0 0.3em;
display: none;
}
table#PoolViewerTable td.Matrix > a[title~="[Timeout]"]::after {
content: "T";
font-size: 0.5em;
}
`;
const SKIN_GREY=`\
body {
background: #f8f8f8;
font-family: Georgia, Times, Times New Roman, serif;
}
table#pageHead, table#pageFoot {
border: solid 1px #707070;
background: #d0d0d0;
color: #202020;
margin-top: -2px;
}
table#pageHead a, table#pageFoot a {
color: #202020;
}
table#pageMenu {
background: #f8f8f8;
border-color: #707070;
}
span.MainMenuCount {
color: #a0a0a0;
}
table.GameInfos tr {
background: #eeeeee;
}
table.Links a {
color: #202020;
}
a[target] {
color: initial;
}
a {
color: initial;
}
table.Goban img.brdl, table.Goban img.brdn, table.Goban img[src='images/blank.gif'] {
filter: grayscale() brightness(102%);
}
td.Logo1 img[alt='Dragon'] {
filter: grayscale() brightness(101%);
}
img[alt='Dragon Go Server'] {
filter: grayscale() brightness(68.5%) contrast(300%);
}
img[alt='Samuraj Logo'] {
filter: grayscale() brightness(97%);
margin-top: -10px;
}
img[alt='Rating graph'] {
filter: grayscale();
}
table.Table tr.Row2, table.Table tr.Row4 {
background: #f4f4f4;
}
table.Table tr.Row1 td.RemTimeWarn2, table.Table tr.Row2 td.RemTimeWarn2,
table.Table tr.Row3 td.RemTimeWarn2, table.Table tr.Row4 td.RemTimeWarn2 {
background: initial;
}
table.Table tr.Row1 td.RemTimeWarn1, table.Table tr.Row2 td.RemTimeWarn1,
table.Table tr.Row3 td.RemTimeWarn1, table.Table tr.Row4 td.RemTimeWarn1 {
background: #fa96a6;
}
h3.Header, h3.Header .Rating, h3.Header .User {
color: initial;
}
table.Infos, table.InfoBox, table.MessageBox {
border-collapse: collapse;
border-color: #707070;
}
table.Infos td, table.InfoBox td, table.MessageBox td, table.Infos th, table.Infos td {
border-color: #707070;
}
img[alt='RSS'] {
display: none;
}
font[color] {
color: initial;
}
table.Table td.Sgf a {
color: initial;
}
td.ServerHome {
color: rgba(0,0,0,0);
}
td.ServerHome select, td.ServerHome input {
display: none;
}
table#PoolViewerTable tr.Empty, table#PoolViewerTable tr.Title {
background: initial;
}
dl.ExtraInfos dd.Guidance, dl.ExtraInfos dd.Info {
color: initial;
}
table.Infos th {
color: initial;
}
table.GameNotes th {
background: #d0d0d0;
color: initial;
}
div#loadingInfo {
background: #d0d0d0;
color: #202020;
border: solid 1px #707070;
}
`;
const RELOAD_INTERVAL=10*60*1000;
class LoadingIndicator{
constructor(){
this.counter_=0;
this.element_=null;
}
static create(){
return new LoadingIndicator();
}
init(){
this.element_=document.createElement("div");
this.element_.id="loadingInfo";
this.element_.title="Working...";
this.element_.innerHTML="...";
document.body.appendChild(this.element_);
this.setVisibility_();
}
registerPromise(loadingPromise){
this.counter_++;
this.setVisibility_();
loadingPromise.catch(()=>null).then(()=>{
setTimeout(()=>{
this.counter_--;
this.setVisibility_();
},10);
});
return loadingPromise;
}
registerStart(){
let done;
this.registerPromise(new Promise(success=>{
done=()=>success(null);
}));
return done;
}
setVisibility_(){
if(this.element_)
this.element_.style.display=this.counter_?"block":null;
}
}
const LOADING_INDICATOR=LoadingIndicator.create();
async function ajax(path,params={},init={}){
const searchParams=new URLSearchParams();
for(const param of Object.keys(params))
searchParams.set(param,params[param]);
const paramsStr=searchParams.toString();
const done=LOADING_INDICATOR.registerStart();
try{
const response=await fetch(
`${path}${paramsStr?`?${paramsStr}`:``}`,
Object.assign({credentials:"include"},init));
return response.ok?response.text():Promise.reject(response);
}finally{
done();
}
}
class ParseError extends Error{}
const coord={
SGFPattern:"[a-s]{2}",
fromSGF(str){
if(!str.match(/^[a-s]{2}$/))
throw new Error(`Bad SGF coordinates format: ${str}`);
const x=str.charCodeAt(0)-96;
const y=str.charCodeAt(1)-96;
return `${String.fromCharCode((x>=9?x+1:x)+96)}${20-y}`;
},
validate(str){
if(!str.match(/^[a-hj-t]([1-9]|1[0-9])$/))
throw new ParseError(`Bad move coordinates: ${str||"(empty)"}`);
},
toSGF(str){
this.validate(str);
let xc=str.charCodeAt(0)-96;
const x=xc>=9?xc-1:xc;
const y=20-parseInt(str.substr(1),10);
return String.fromCharCode(x+96,y+96);
},
};
class CondMoves{
constructor(gameId,atMoveNo,tree={}){
this.gameId=gameId;
this.atMoveNo=atMoveNo;
this.tree=tree;
}
static parseUserString(gameId,atMoveNo,str){
const condMoves=new CondMoves(gameId,atMoveNo);
for(const path of str.trim().split(/\s*[,;\n]\s*/)){
if(!path)
continue;
const moves=path.split(/\s+/);
for(const move of moves)
coord.validate(move);
if(moves.length%2!==0)
throw new ParseError(`Path with odd length (no response specified for opponent's last move): ${moves.join(" ")}`);
let cm=condMoves;
for(let i=0;i<moves.length;i+=2){
const branch=cm.getBranch_(moves[i],moves[i+1]);
cm=branch.condMoves;
}
}
return condMoves;
}
getBranch_(move,response){
let branch=this.tree[move];
if(branch&&branch.response!==response)
throw new ParseError(`Inconsistent response to ${move} in different branches: ${branch.response} vs ${response}`);
if(!branch){
branch={response,condMoves:new CondMoves(this.gameId,this.atMoveNo+2)};
this.tree[move]=branch;
}
return branch;
}
hasMoves(){
return !!Object.keys(this.tree).length;
}
toUserString(){
return this.toUserStringHelper_().map(path=>path.join(" ")).join(", ");
}
toUserStringHelper_(){
const paths=[];
if(this.hasMoves()){
for(const move of Object.keys(this.tree)){
const branch=this.tree[move];
for(const path of branch.condMoves.toUserStringHelper_())
paths.push([move,branch.response,...path]);
}
}else
paths.push([]);
return paths;
}
toString(){
return `CondMoves[${this.gameId}@${this.atMoveNo}: ${this.toUserString()}]`;
}
serialise(){
return `@${this.atMoveNo} ${this.toUserString()}`;
}
static deserialise(gameId,str){
const mat=str.match(/^\s*@(\d+) (.*)$/);
if(!mat)
throw new Error(`Bad serialised conditional moves: ${str}`);
return CondMoves.parseUserString(gameId,parseInt(mat[1],10),mat[2]);
}
}
/*
class Storage{
constructor(base,fieldsWithDefaults,serialiser=JSON){
const serialise=(serialiser.serialise||serialiser.stringify).bind(serialiser);
for(const field of Object.keys(fieldsWithDefaults)){
const defVal=fieldsWithDefaults[field];
Object.defineProperty(this,field,{
get:()=>{
const str=base.getItem(field);
if(str===null)
return defVal;
return serialiser.parse(str);
},
set:v=>{
base.setItem(field,serialise(v));
},
});
}
}
}
const LOCAL_STORAGE=new Storage(localStorage,{});
*/
class PrivateNotes{
constructor(gameId,base,condMoves){
this.gameId=gameId;
this.base=base;
this.condMoves=condMoves;
}
static parse(gameId,text){
const mat=text.match(/^([\s\S]*?)(?:(?:^|\n)Conditional moves: (.+)\n*)?$/);
let base=mat[1];
let condMoves=null;
if(mat[2])
try{
condMoves=CondMoves.deserialise(gameId,mat[2]);
if(!(condMoves instanceof CondMoves))
throw new Error(`Expected CondMoves, got ${condMoves}`);
}catch(e){
console.warn(`Cannot parse conditional moves from private notes: ${mat[2]}, error: ${e}`);
base=mat[0];
condMoves=null;
}
base=base.trim();
if(base)
base+="\n";
return new PrivateNotes(gameId,base,condMoves);
}
static empty(gameId){
return new PrivateNotes(gameId,"",null);
}
async saveCondMoves(condMoves){
this.condMoves=condMoves;
let text=this.base;
if(condMoves&&condMoves.hasMoves())
text+=`\n\n\nConditional moves: ${condMoves.serialise()}\n`;
await ajax("/quick_do.php",{
obj:"game",
cmd:"save_notes",
gid:this.gameId,
notes:text,
});
ajax("/quick_do.php",{
obj:"game",
cmd:"hide_notes",
gid:this.gameId,
});
}
toString(){
return `PrivateNotes[condMoves=${this.condMoves}]`;
}
}
class GameSGF{
constructor(gameId,moveNo,lastMove,privateNotes,hasExtraComments){
this.gameId=gameId;
this.moveNo=moveNo;
this.lastMove=lastMove;
this.privateNotes=privateNotes;
this.hasExtraComments=hasExtraComments;
}
get condMoves(){
return this.privateNotes&&this.privateNotes.condMoves;
}
static parse(gameId,sgf){
const nodes=sgf.split("\n;").slice(1);
const firstNodeTags=GameSGF.parseTags_(nodes[0]);
const moveNo=firstNodeTags.XM?parseInt(firstNodeTags.XM,10):null;
const lastNodeTags=GameSGF.parseTags_(nodes[nodes.length-1]);
const lastMoveSGF=lastNodeTags.B||lastNodeTags.W;
const lastMove=lastMoveSGF?coord.fromSGF(lastMoveSGF):null;
const lastMoveComment=lastNodeTags.C;
let privateNotes=null;
let hasExtraComments=false;
if(lastMoveComment){
const nMat=lastMoveComment.match(/(?:^|\n)Notes - .+? \(.+?\):\n([\s\S]*)/);
if(nMat){
privateNotes=PrivateNotes.parse(gameId,nMat[1]);
hasExtraComments=nMat[0].trim()!==lastMoveComment.trim();
}else
hasExtraComments=true;
}
return new GameSGF(gameId,moveNo,lastMove,privateNotes,hasExtraComments);
}
static async load(gameId,allowCache=false){
return GameSGF.parse(gameId,await ajax("/sgf.php",{
gid:gameId,
owned_comments:1,
quick_mode:1,
no_cache:allowCache?0:1,
}));
}
static parseTags_(node){
const regexp=/([A-Z]{1,2})\[/g;
const result={};
for(;;){
const mat=regexp.exec(node);
if(!mat)
break;
let value="";
let i=regexp.lastIndex;
for(;;){
if(i>=node.length)
break;
if(node[i]==="]"&&!(i+1<node.length&&node[i+1]==="["))
break;
if(node[i]==="\\"&&i+1<node.length&&(node[i+1]==="\\"||node[i+1]==="["))
value+=node[++i];
else
value+=node[i];
i++;
}
result[mat[1]]=value;
regexp.lastIndex=i;
}
return result;
}
async executeCondMoves(){
if(!this.condMoves||!this.moveNo||!this.lastMove)
return {};
const {clearCondMoves,condMoves:newCondMoves,response}=this.analyseCondMoves_();
let condMovesToSave=clearCondMoves?null:newCondMoves;
let promise;
if(response){
console.debug(`Responding in game ${this.gameId} to ${this.lastMove} with ${response}`);
promise=ajax("/quick_do.php",{
obj:"game",
cmd:"move",
gid:this.gameId,
move_id:this.moveNo,
move:coord.toSGF(response),
},{method:"POST"}).then(()=>({responded:true}));
promise.catch(error=>{
console.warn(`Responding in game ${this.gameId} at move ${this.moveNo} with ${response} failed: ${error}`);
condMovesToSave=null;
});
}else
promise=Promise.resolve({});
promise.catch(()=>null).then(()=>this.saveCondMoves(condMovesToSave));
return promise;
}
analyseCondMoves_(){
if(this.hasExtraComments){
console.debug(`In game ${this.gameId} additional information is associated with last move; clearing conditional moves`);
return {clearCondMoves:true};
}
if(this.moveNo!==this.condMoves.atMoveNo){
console.debug(`In game ${this.gameId} current move is ${this.moveNo}, but conditional moves defined for move ${this.condMoves.atMoveNo}; clearing conditional moves`);
return {clearCondMoves:true};
}
const branch=this.condMoves.tree[this.lastMove];
if(!branch){
console.debug(`In game ${this.gameId} no response defined for ${this.lastMove}; clearing conditional moves`);
return {clearCondMoves:true};
}
return branch;
}
async checkCondMovesOnOpponentTurn(){
if(!this.condMoves||!this.moveNo)
return null;
if(this.moveNo>this.condMoves.atMoveNo){
console.debug(`In game ${this.gameId} current move is ${this.moveNo}, but conditional moves defined for move ${this.condMoves.atMoveNo}; clearing conditional moves`);
return this.saveCondMoves(null);
}
return null;
}
async saveCondMoves(condMoves){
if(!this.privateNotes)
this.privateNotes=PrivateNotes.empty(this.gameId);
return this.privateNotes.saveCondMoves(condMoves);
}
toString(){
return `GameSGF[gameId=${this.gameId}, @${this.moveNo}, lastMove=${this.lastMove
}, privateNotes=${this.privateNotes}, hasExtraComments=${this.hasExtraComments}]`;
}
}
class QuickStatus{
constructor(objects){
this.objects_=objects;
}
static parse(status){
const errMat=status.match(/\[#Error: (.+?)\]$/m);
if(errMat)
throw new Error(`QuickStatus parse error: ${errMat[1]}`);
const objectsByType=new Map();
const headersByType=new Map();
const getFields=str=>{
const fields=[];
let field="";
let quoted=false;
for(let i=0;i<str.length;i++){
if(str[i]==="'"){
if(quoted)
quoted=false;
else if(field)
throw new Error(`Unexpected quote in string: ${str}`);
else
quoted=true;
}else if(str[i]==="\\"&"ed){
if(++i>=str.length)
throw new Error(`Unexpected end of string: ${str}`);
field+=str[i];
}else if(str[i]===","&&!quoted){
fields.push(field);
field="";
}else
field+=str[i];
}
fields.push(field);
return fields;
};
const scan=function*(str,regexp){
let mat;
while(mat=regexp.exec(str))
yield mat;
};
for(const mat of scan(status,/^## ([A-Z]),(.+?)$/mg))
headersByType.set(mat[1],getFields(mat[2]));
for(const mat of scan(status,/^([A-Z]),(.+?)$/mg)){
const type=mat[1];
let objects=objectsByType.get(type);
if(!objects){
objects=[];
objectsByType.set(type,objects);
}
const headers=headersByType.get(type);
const fields=getFields(mat[2]);
const object={};
objects.push(object);
for(let i=0;i<headers.length;i++)
object[headers[i]]=fields[i];
}
return new QuickStatus(objectsByType);
}
static async load(){
return QuickStatus.parse(await ajax("/quick_status.php",{version:2,no_cache:1,order:0}));
}
get messages(){
return this.objects_.get("M")||[];
}
get games(){
return this.objects_.get("G")||[];
}
}
class TitleUpdater{
constructor(base,initialCount){
this.base_=base;
this.initialCount_=initialCount;
}
static create(){
const mat=document.title.match(/^(.+?)(?: \((\d+)\))?$/)
return new TitleUpdater(mat[1],mat[2]||null);
}
quickUpdate(){
this.updateInternal_(this.initialCount_,0);
}
update(quickStatus){
this.updateInternal_(quickStatus.games.length,quickStatus.messages.length);
}
updateInternal_(gamesCount,messagesCount){
document.title=`${gamesCount==null?``:`[${gamesCount}${messagesCount?`, ${messagesCount}`:``}] `}${this.base_}`;
}
}
class Manager{
constructor(titleUpdater){
this.titleUpdater_=titleUpdater;
this.quickStatus=null;
}
static start(){
const titleUpdater=TitleUpdater.create();
titleUpdater.quickUpdate();
const manager=new Manager(titleUpdater);
const update=async()=>{
try{
await manager.update();
}finally{
scheduleUpdate();
}
};
const scheduleUpdate=()=>setTimeout(update,RELOAD_INTERVAL);
scheduleUpdate();
return manager;
}
async update(){
if(location.pathname==="/index.php")
return;
if(location.pathname==="/status.php")
location.reload();
else{
this.quickStatus=await QuickStatus.load();
this.titleUpdater_.update(this.quickStatus);
Promise.all(this.quickStatus.games.map(game=>
GameSGF.load(game.game_id).then(sgf=>sgf.executeCondMoves()).catch(error=>{
console.warn(`Error while executing conditional move for game ${gameId}: ${error}`);
return {error};
}))).then(results=>{
if(results.some(result=>result.responded))
this.update();
});
}
}
}
function addCSS(css){
const styleElem=document.createElement("style");
styleElem.innerText=css;
document.head.appendChild(styleElem);
}
async function start(){
const COND_MOVES_TR_INNER_HTML=`\
<td class="UnderBoard">
<div id="condMoves" title="Specify branches of conditional moves, e.g.: f3 c6 d2 c3, c6 f3
Right-click on board to enter coordinates">
<span>Conditional moves:</span>
<input id="condMoves" type="text">
<button id="saveCondMoves" type="button">Save</button>
</div>
</td>`;
LOADING_INDICATOR.init();
const manager=Manager.start();
const keyHandlers=new Map();
if(location.pathname==="/status.php"){
Promise.all([...document.querySelectorAll("table#gameTable tr td.Button:first-child")].map(gameIdElem=>{
const gameId=gameIdElem.innerText.trim();
return GameSGF.load(gameId).then(sgf=>sgf.executeCondMoves()).catch(error=>{
console.warn(`Error while executing conditional move for game ${gameId}: ${error}`);
return {error};
});
})).then(results=>{
if(results.some(result=>result.responded))
location.reload();
});
}else if(location.pathname==="/game.php"){
const gameId=location.searchParams.get("g")||location.searchParams.get("gid");
let gameState;
if(location.searchParams.get("a")==="domove")
gameState="confirmMove";
else if(location.searchParams.get("a")==="resign")
gameState="resigning";
else if(document.querySelector("dl.ExtraInfos dd.Score"))
gameState="finished";
else if(document.querySelector("input[name='action'][value='choose_move']"))
gameState="myMove";
else
gameState="theirMove";
const eidogoLinkImg=document.querySelector("a.NoPrint > img[title='EidoGo Game Player']");
if(eidogoLinkImg)
eidogoLinkImg.parentElement.setAttribute("target","_blank");
if(gameState==="confirmMove"){
const linkifyField=(imageElement,href)=>{
if(!imageElement)
return;
const td=imageElement.parentElement;
const link=document.createElement("a");
link.setAttribute("href",href);
link.appendChild(imageElement);
td.appendChild(link);
};
const moveParams=new URLSearchParams(location.searchParams);
for(const mark of [".",","]){
for(const img of document.querySelectorAll(`table#Goban td[id].brdx img[alt='${mark}'].brdx`)){
moveParams.set("c",img.parentElement.id);
linkifyField(img,`/game.php?${moveParams}`);
}
}
moveParams.delete("a");
moveParams.delete("c");
for(const mark of ["@","#"])
linkifyField(document.querySelector(`table#Goban td[id].brdx img[alt='${mark}'].brdx`),`/game.php?${moveParams}`);
const cancelMove=()=>document.querySelector("input[name='cancel']").click();
keyHandlers.set("ArrowLeft",cancelMove);
keyHandlers.set("Home",cancelMove);
keyHandlers.set("End",cancelMove);
}else{
const navigate=(selOptFunc)=>{
const selMoveOption=document.querySelector("select[name='gotomove'] option[selected]");
if(!selMoveOption)
return;
const newMoveOption=selOptFunc(selMoveOption);
if(newMoveOption&&newMoveOption!==selMoveOption){
selMoveOption.removeAttribute("selected");
newMoveOption.setAttribute("selected","");
document.querySelector("input[name='movechange']").click();
}
};
keyHandlers.set("ArrowLeft",()=>navigate(o=>o.nextElementSibling));
keyHandlers.set("ArrowRight",()=>navigate(o=>o.previousElementSibling));
keyHandlers.set("Home",()=>navigate(o=>o.parentElement.lastElementChild));
keyHandlers.set("End",()=>navigate(o=>o.parentElement.firstElementChild));
}
if(gameState==="confirmMove"||(gameState==="theirMove"&&
document.querySelector("select[name='gotomove'] option[selected]:first-of-type"))){
const condMovesRow=document.createElement("tr");
condMovesRow.innerHTML=COND_MOVES_TR_INNER_HTML;
const summaryRow=document.querySelector("table#GamePage > tbody > tr:nth-of-type(2)");
condMovesRow.querySelector("td").setAttribute("colspan",summaryRow.querySelector("td").getAttribute("colspan"));
if(gameState==="confirmMove")
condMovesRow.querySelector("#saveCondMoves").style.display="none";
const condMovesInput=condMovesRow.querySelector("input#condMoves");
summaryRow.parentElement.insertBefore(condMovesRow,summaryRow);
const sgfPromise=GameSGF.load(gameId);
sgfPromise.then(sgf=>{
if(sgf.condMoves)
condMovesInput.value=sgf.condMoves.toUserString();
});
const moveNoOffset=gameState==="confirmMove"?2:1;
const saveCondMoves=()=>{
const promise=sgfPromise.then(sgf=>{
const condMoves=CondMoves.parseUserString(gameId,sgf.moveNo+moveNoOffset,condMovesInput.value);
condMovesInput.value=condMoves.toUserString();
return sgf.saveCondMoves(condMoves);
});
promise.catch(error=>{
let msg;
if(error instanceof ParseError)
msg=`Invalid conditional moves: ${error.message}`;
else
msg=`Error saving conditional moves: ${error}`;
console.warn(msg);
alert(msg);
});
return promise;
};
condMovesRow.querySelector("#saveCondMoves").addEventListener("click",()=>{
saveCondMoves();
return true;
});
condMovesInput.addEventListener("keydown",event=>{
if(event.code==="Enter"){
saveCondMoves();
event.preventDefault();
}
});
const rightClickHandler=event=>{
if(event.button===2){
const move=coord.fromSGF(event.currentTarget.id);
const text=condMovesInput.value;
const focused=document.activeElement===condMovesInput;
let selRange;
if(focused)
selRange=[text.length,text.length];
else
selRange=[condMovesInput.selectionStart,condMovesInput.selectionEnd];
const preText=text.substring(0,selRange[0]);
const postText=text.substring(selRange[1]);
let newText=preText;
if(preText.match(/\S$/))
newText+=" ";
newText+=move;
const newCursorPos=newText.length;
if(postText.match(/^[^,\s]/))
newText+=" ";
newText+=postText;
condMovesInput.value=newText;
condMovesInput.focus();
condMovesInput.setSelectionRange(newCursorPos,newCursorPos);
event.preventDefault();
}
};
for(const field of document.querySelectorAll("table#Goban td[id].brdx")){
field.addEventListener("mouseup",rightClickHandler);
field.addEventListener("contextmenu",event=>event.preventDefault());
}
let allowedSubmitButton=null;
const submitHandler=event=>{
if(event.target===allowedSubmitButton)
return;
saveCondMoves().then(()=>{
allowedSubmitButton=event.target;
event.target.click();
});
event.preventDefault();
};
for(const name of ["nextgame","nextstatus"]){
const button=document.querySelector(`input[type='submit'][name='${name}']`);
if(button)
button.addEventListener("click",submitHandler);
}
}
keyHandlers.set("Space",()=>{
const skipParams=new URLSearchParams();
skipParams.set("gid",gameId);
skipParams.set("nextskip","t");
location.href=`/confirm.php?${skipParams}`;
});
}else if(location.pathname==="/show_games.php"){
for(const gameIdElem of document.querySelectorAll("table#runningTable tr td.Button:first-child")){
const infoElem=gameIdElem.parentElement.querySelector("td.ImagesLeft");
if(infoElem){
const gameId=gameIdElem.innerText.trim();
GameSGF.load(gameId).then(sgf=>{
return sgf.checkCondMovesOnOpponentTurn().then(()=>{
if(sgf.privateNotes&&sgf.privateNotes.base.trim())
infoElem.innerHTML+=` <span class="game-info-char" title="There are private notes saved for this game">(p)</span>`;
if(sgf.condMoves)
infoElem.innerHTML+=` <span class="game-info-char" title="Conditional moves are defined for this game">(c)</span>`;
});
});
}
}
}else if(location.pathname==="/error.php")
setTimeout(()=>location.href="/status.php",3600*1000);
keyHandlers.set("Escape",()=>location.href=`/status.php`);
if(keyHandlers.size){
document.addEventListener("keydown",event=>{
if(["INPUT","TEXTAREA"].includes(event.target.tagName))
return;
const code=event.code;
const handler=keyHandlers.get(code);
if(handler){
event.preventDefault();
handler();
}
});
}
}
async function onLoaded(){
addCSS(EXTRA_CSS);
addCSS(SKIN_GREY);
location.searchParams=new URLSearchParams(location.search);
try{
await start();
}catch(e){
console.error(e);
}
}
if(document.readyState==="loading")
document.addEventListener("DOMContentLoaded",onLoaded,false);
else
onLoaded();
})();