mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2024-12-29 10:26:36 +01:00
527 lines
14 KiB
JavaScript
527 lines
14 KiB
JavaScript
var state = {client: null,
|
|
closure: null,
|
|
connected: false,
|
|
disconnTimeMS: 0,
|
|
connTimeMS: 0,
|
|
connChangeStamp: 0,
|
|
};
|
|
|
|
function fontSize(adj=1.0) {
|
|
return (adj * state.height / 500) + 'rem';
|
|
}
|
|
|
|
function ccallString(proc, closure, str) {
|
|
Module.ccall('cbckString', null, ['number', 'number', 'string'],
|
|
[proc, closure, str]);
|
|
}
|
|
|
|
function registerOnce(devid, gitrev, now, dbg) {
|
|
let nextTimeKey = 'next_reg';
|
|
let gitKey = 'last_write';
|
|
let nextTime = parseInt(localStorage.getItem(nextTimeKey));
|
|
let prevGit = localStorage.getItem(gitKey);
|
|
if ( prevGit == gitrev && now < nextTime ) {
|
|
if ( dbg ) {
|
|
console.log('registerOnce(): next in ' + (nextTime - now) + ' secs');
|
|
}
|
|
} else {
|
|
let vrntName = window.location.host;
|
|
vrntName += window.location.pathname.split('/').slice(0, -1).join('/');
|
|
let args = { devid: devid,
|
|
gitrev: gitrev,
|
|
loc: navigator.language,
|
|
os: navigator.appName,
|
|
vers: '0.0',
|
|
dbg: dbg,
|
|
myNow: now,
|
|
vrntName: vrntName,
|
|
};
|
|
let body = JSON.stringify(args);
|
|
|
|
fetch('/xw4/api/v1/register', {
|
|
method: 'post',
|
|
body: body,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
}).then(handleFetchErrors)
|
|
.then(res => {
|
|
return res.json();
|
|
}).then(data => {
|
|
// console.log('data: ' + JSON.stringify(data));
|
|
if ( data.success ) {
|
|
localStorage.setItem(nextTimeKey, data.atNext);
|
|
localStorage.setItem(gitKey, gitrev);
|
|
}
|
|
}).catch(ex => {
|
|
console.error('registerOnce(): fetch()=>' + ex);
|
|
});
|
|
}
|
|
}
|
|
|
|
function postAck(sum, gameID) {
|
|
fetch('/xw4/api/v1/ack', {
|
|
method: 'post',
|
|
body: JSON.stringify({ "sum": sum, "gid": gameID, }),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
})
|
|
.then(handleFetchErrors)
|
|
.then(res => {
|
|
return res.json();
|
|
}).then(data => {
|
|
console.log('data from ack: ' + JSON.stringify(data));
|
|
}).catch(ex => {
|
|
console.error('postAck(): fetch()=>' + ex);
|
|
});
|
|
}
|
|
|
|
function handleFetchErrors(response) {
|
|
if ( response.ok ) {
|
|
return response;
|
|
} else {
|
|
throw Error(response.statusText);
|
|
}
|
|
}
|
|
|
|
function getDict(langs, proc, closure) {
|
|
function callWhenDone(xwd, lc, langName, data, len) {
|
|
Module.ccall('gotDictBinary', null,
|
|
['number', 'number', 'string', 'string', 'string', 'array', 'number'],
|
|
[proc, closure, xwd, lc, langName, data, len ]);
|
|
}
|
|
|
|
let gots = {}; // for later
|
|
|
|
console.log('langs: ' + langs + '; langs[0]: ' + langs[0]);
|
|
let args = '?lc=' + langs.join('|');
|
|
console.log('args: ' + args);
|
|
fetch('/xw4/info.py/listDicts' + args, {
|
|
method: 'post',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}).then(handleFetchErrors)
|
|
.then(response => {
|
|
return response.json();
|
|
}).then(data => {
|
|
// console.log('data: ' + JSON.stringify(data));
|
|
for ( lang of data.langs ) {
|
|
let dict = null;
|
|
for ( one of lang.dicts ) {
|
|
if ( !dict || one.nBytes > dict.nBytes ) {
|
|
dict = one;
|
|
}
|
|
}
|
|
if ( dict ) {
|
|
gots.xwd = dict.xwd;
|
|
gots.langName = lang.lang;
|
|
gots.lc = lang.lc;
|
|
let path = '/' + ['and_wordlists', gots.langName, gots.xwd].join('/');
|
|
return fetch(path);
|
|
}
|
|
}
|
|
}).then(handleFetchErrors)
|
|
.then(response => {
|
|
// console.log('got here!!!' + response);
|
|
return response.arrayBuffer();
|
|
}).then(data=> {
|
|
let len = data.byteLength;
|
|
let dataPtr = Module._malloc(len);
|
|
// Copy data to Emscripten heap
|
|
var dataHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, len);
|
|
dataHeap.set( new Uint8Array(data) );
|
|
callWhenDone(gots.xwd, gots.lc, gots.langName, dataHeap, len);
|
|
Module._free(dataPtr);
|
|
}).catch(ex => {
|
|
callWhenDone(null, null, null, [], 0);
|
|
});
|
|
}
|
|
|
|
// Called from main() asap after things are initialized etc.
|
|
function jssetup(closure, dbg, devid, gitrev, now, noTabProc,
|
|
focusProc, msgProc, subTopics) {
|
|
// Set a unique tag so we know if somebody comes along later
|
|
let tabID = Math.random();
|
|
let item = 'tabID/' + dbg;
|
|
localStorage.setItem(item, tabID);
|
|
let listener = function () {
|
|
newTabID = localStorage.getItem(item);
|
|
if ( newTabID != tabID ) {
|
|
state.client.disconnect();
|
|
ccallString(noTabProc, state.closure, '');
|
|
window.removeEventListener('storage', listener);
|
|
}
|
|
};
|
|
window.addEventListener('storage', listener);
|
|
|
|
window.onfocus = function () {
|
|
ccallString(focusProc, state.closure, 'focus');
|
|
};
|
|
window.onblur = function () {
|
|
ccallString(focusProc, state.closure, 'blur');
|
|
};
|
|
|
|
state.closure = closure;
|
|
state.msgProc = msgProc;
|
|
state.subTopics = subTopics;
|
|
|
|
registerOnce(devid, gitrev, now, dbg);
|
|
|
|
document.getElementById("git_rev").textContent = gitrev;
|
|
document.getElementById("mqtt_span").textContent = devid;
|
|
|
|
function onConnChange(isConn) {
|
|
state.connected = isConn;
|
|
|
|
const now = Date.now();
|
|
if ( 0 != state.connChangeStamp ) {
|
|
const incr = now - state.connChangeStamp;
|
|
if ( isConn ) {
|
|
state.disconnTimeMS += incr;
|
|
} else {
|
|
state.connTimeMS += incr;
|
|
}
|
|
}
|
|
state.connChangeStamp = now;
|
|
|
|
let stateStr = isConn ? 'Connected' : 'Disconnected';
|
|
document.getElementById("mqtt_status").textContent = stateStr;
|
|
|
|
Module.ccall('MQTTConnectedChanged', null, ['number', 'boolean'],
|
|
[state.closure, isConn]);
|
|
|
|
console.error('new conn state: ', isConn, '; total conn: ', state.connTimeMS,
|
|
'; total disconn: ', state.disconnTimeMS );
|
|
}
|
|
|
|
state.client = new Paho.MQTT.Client("eehouse.org", 9001, '/wss', devid);
|
|
|
|
// set callback handlers
|
|
state.client.onConnectionLost = function onConnectionLost(responseObject) {
|
|
onConnChange(false);
|
|
if (responseObject.errorCode !== 0) {
|
|
console.log("onConnectionLost:"+responseObject.errorMessage);
|
|
}
|
|
};
|
|
state.client.onMessageArrived = function onMessageArrived(message) {
|
|
let topic = message.topic;
|
|
let payload = message.payloadBytes;
|
|
let length = payload.length;
|
|
Module.ccall('cbckMsg', null, ['number', 'number', 'string', 'number', 'array'],
|
|
[state.msgProc, state.closure, topic, length, payload]);
|
|
};
|
|
|
|
function onConnect() {
|
|
onConnChange(true);
|
|
|
|
var subscribeOptions = {
|
|
qos: 2, // QoS
|
|
// invocationContext: {foo: true}, // Passed to success / failure callback
|
|
// onSuccess: function() { alert('subscribe succeeded'); },
|
|
onFailure: function() { alert('subscribe failed'); },
|
|
timeout: 10,
|
|
};
|
|
|
|
for ( topic of state.subTopics ) {
|
|
// console.log('calling subscribe('+topic+')');
|
|
state.client.subscribe(topic, subscribeOptions);
|
|
}
|
|
}
|
|
|
|
state.client.connect({mqttVersion: 3,
|
|
userName: "xwuser",
|
|
password: "xw4r0cks",
|
|
useSSL: true,
|
|
reconnect: true,
|
|
onSuccess: onConnect,
|
|
onFailure: function() { console.error('mqtt.client.connect.onFailure'); },
|
|
});
|
|
|
|
function callResize() {
|
|
state.width = window.innerWidth;
|
|
state.height = state.width * 2;
|
|
if ( state.height > window.innerHeight ) {
|
|
state.height = window.innerHeight;
|
|
state.width = state.height / 2;
|
|
}
|
|
state.height = state.height * 100 / 151;
|
|
|
|
for ( const id of ['nbalert', 'gamename'] ) {
|
|
document.getElementById(id).style['font-size'] = fontSize();
|
|
}
|
|
|
|
ccall('onResize', null, ['number', 'number', 'number'],
|
|
[state.closure, state.width, state.height]);
|
|
}
|
|
window.addEventListener('resize', function() {
|
|
const innerWidth = window.innerWidth;
|
|
const innerHeight = window.innerHeight;
|
|
window.setTimeout( function() {
|
|
if ( innerWidth == window.innerWidth && innerHeight == window.innerHeight ) {
|
|
callResize();
|
|
}
|
|
}, 500 );
|
|
});
|
|
callResize(); // so client knows initial size
|
|
}
|
|
|
|
function mqttSend( topic, ptr ) {
|
|
let canSend = null != state.client && state.connected;
|
|
if ( canSend ) {
|
|
message = new Paho.MQTT.Message(ptr);
|
|
message.destinationName = topic;
|
|
message.qos = 2;
|
|
message.retained = true;
|
|
state.client.send(message);
|
|
} else {
|
|
console.error('mqttSend: not connected');
|
|
}
|
|
return canSend;
|
|
}
|
|
|
|
function addDepthNote(dlg) {
|
|
let depth = dlg.parentNode.childElementCount;
|
|
if ( depth > 1 ) {
|
|
let div = document.createElement('div');
|
|
div.textContent = '(Depth: ' + depth + ')';
|
|
dlg.appendChild(div);
|
|
}
|
|
}
|
|
|
|
function newDlgWMsg(msg) {
|
|
let container = document.getElementById('nbalert');
|
|
|
|
let dlg = document.createElement('div');
|
|
dlg.classList.add('nbalert');
|
|
dlg.style.zIndex = 10000 + container.childElementCount;
|
|
container.appendChild( dlg );
|
|
|
|
let txtDiv = document.createElement('div');
|
|
txtDiv.textContent = msg
|
|
dlg.appendChild( txtDiv );
|
|
|
|
return dlg;
|
|
}
|
|
|
|
function newButtonDiv(buttons, proc, asDivs) {
|
|
let div = document.createElement('div');
|
|
div.classList.add('buttonRow');
|
|
for ( let ii = 0; ii < buttons.length; ++ii ) {
|
|
let buttonTxt = buttons[ii];
|
|
let button = document.createElement('button');
|
|
button.classList.add('xwbutton');
|
|
button.textContent = buttonTxt;
|
|
button.style['font-size'] = fontSize(1.2);
|
|
button.onclick = function() { proc(ii); };
|
|
if ( asDivs ) {
|
|
let bdiv = document.createElement('div');
|
|
bdiv.appendChild(button);
|
|
button = bdiv;
|
|
}
|
|
div.appendChild(button);
|
|
}
|
|
|
|
return div;
|
|
}
|
|
|
|
function nbDialog(msg, buttons, proc, closure) {
|
|
const dlg = newDlgWMsg( msg );
|
|
|
|
const butProc = function(indx) {
|
|
dlg.parentNode.removeChild(dlg);
|
|
ccallString(proc, closure, buttons[indx]);
|
|
}
|
|
dlg.appendChild( newButtonDiv( buttons, butProc, false ) );
|
|
addDepthNote(dlg);
|
|
}
|
|
|
|
function nbBlankPick(title, buttons, proc, closure) {
|
|
let dlg = newDlgWMsg( title );
|
|
|
|
const butProc = function(indx) {
|
|
dlg.parentNode.removeChild(dlg);
|
|
ccallString(proc, closure, indx.toString());
|
|
}
|
|
|
|
dlg.appendChild( newButtonDiv( buttons, butProc, false ) );
|
|
addDepthNote(dlg);
|
|
}
|
|
|
|
// To enable sorting of names on buttons while keeping existing code,
|
|
// I'm creating an array of pairs then sorting that.
|
|
function nbGamePick(title, gameMap, proc, closure) {
|
|
let dlg = newDlgWMsg( title );
|
|
|
|
let pairs = [];
|
|
Object.keys(gameMap).forEach( function(key) {
|
|
pairs.push({id:key, txt:gameMap[key]});
|
|
});
|
|
|
|
pairs.sort(function(a, b){
|
|
var stra = a.txt.toLowerCase();
|
|
var strb = b.txt.toLowerCase();
|
|
if (stra < strb) {return -1;}
|
|
if (stra > strb) {return 1;}
|
|
return parseInt(a.id) - parseInt(b.id);
|
|
});
|
|
let buttons = [];
|
|
for ( pair of pairs ) {
|
|
buttons.push(pair.txt);
|
|
}
|
|
|
|
butProc = function(indx) {
|
|
dlg.parentNode.removeChild(dlg);
|
|
ccallString(proc, closure, pairs[indx].id);
|
|
}
|
|
dlg.appendChild( newButtonDiv( buttons, butProc, true ) );
|
|
|
|
cancelProc = function() {
|
|
dlg.parentNode.removeChild(dlg);
|
|
ccallString(proc, closure, null);
|
|
};
|
|
dlg.appendChild( newButtonDiv( ['Cancel'], cancelProc, false ) );
|
|
|
|
addDepthNote(dlg);
|
|
}
|
|
|
|
function setDivButtons(divid, buttons, proc, closure) {
|
|
let parent = document.getElementById(divid);
|
|
while ( parent.lastElementChild ) {
|
|
parent.removeChild(parent.lastElementChild);
|
|
}
|
|
|
|
butProc = function(indx) {
|
|
ccallString(proc, closure, buttons[indx]);
|
|
}
|
|
|
|
parent.appendChild( newButtonDiv( buttons, butProc, false ) );
|
|
}
|
|
|
|
function nbGetString(msg, dflt, proc, closure) {
|
|
let dlg = newDlgWMsg( msg );
|
|
|
|
let div = document.createElement('div');
|
|
div.classList.add('emscripten');
|
|
dlg.appendChild(div);
|
|
|
|
let tarea = document.createElement('textarea');
|
|
tarea.classList.add('stringedit');
|
|
tarea.style['font-size'] = fontSize();
|
|
tarea.value = dflt;
|
|
div.appendChild( tarea );
|
|
|
|
dismissed = function(str) {
|
|
dlg.parentNode.removeChild(dlg);
|
|
ccallString(proc, closure, str);
|
|
}
|
|
|
|
let buttons = ["Cancel", "OK"];
|
|
butProc = function(indx) {
|
|
if ( buttons[indx] == 'Cancel' ) {
|
|
dismissed(null);
|
|
} else if ( buttons[indx] == 'OK' ) {
|
|
dismissed(tarea.value);
|
|
}
|
|
}
|
|
dlg.appendChild( newButtonDiv( buttons, butProc, false ) );
|
|
addDepthNote(dlg);
|
|
}
|
|
|
|
function newRadio(txt, id, isSet, proc) {
|
|
let span = document.createElement('span');
|
|
let radio = document.createElement('input');
|
|
radio.type = 'radio';
|
|
radio.id = id;
|
|
radio.name = id;
|
|
radio.onclick = proc;
|
|
radio.checked = isSet;
|
|
|
|
let label = document.createElement('label')
|
|
label.htmlFor = id;
|
|
var description = document.createTextNode(txt);
|
|
label.appendChild(description);
|
|
|
|
span.appendChild(radio);
|
|
span.appendChild(label);
|
|
|
|
return span;
|
|
}
|
|
|
|
function newCheckbox(txt, id, val) {
|
|
let span = document.createElement('span');
|
|
let checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.id = id;
|
|
checkbox.checked = val;
|
|
|
|
let label = document.createElement('label')
|
|
var description = document.createTextNode(txt);
|
|
label.appendChild(description);
|
|
|
|
span.appendChild(checkbox);
|
|
span.appendChild(label);
|
|
|
|
return span;
|
|
}
|
|
|
|
function nbGetNewGame(closure, msg, allowHints, isRobot, langs, langName) {
|
|
const dlg = newDlgWMsg(msg);
|
|
|
|
let hintsDiv = document.createElement('div');
|
|
dlg.appendChild(hintsDiv);
|
|
hintsDiv.appendChild(newCheckbox("Allow hints", 'allowHints', allowHints));
|
|
|
|
const explDiv = document.createElement('div');
|
|
dlg.appendChild( explDiv );
|
|
explDiv.textContent = '>> Is your opponent a robot or someone you will invite?';
|
|
|
|
const radioDiv = document.createElement('div');
|
|
dlg.appendChild( radioDiv );
|
|
let robotSet = isRobot;
|
|
radioDiv.appendChild(newRadio('Robot', 'newgame', robotSet,
|
|
function() {robotSet = true;}));
|
|
radioDiv.appendChild(newRadio('Remote player', 'newgame', !robotSet,
|
|
function() {robotSet = false;} ));
|
|
|
|
let chosenLang = langName ? langName : langs[0];
|
|
if ( 1 < langs.length ) {
|
|
const langsExplDiv = document.createElement('div');
|
|
dlg.appendChild( langsExplDiv );
|
|
langsExplDiv.textContent = ">> Choose your game language";
|
|
const langsDiv = document.createElement('div');
|
|
dlg.appendChild( langsDiv );
|
|
for ( let ii = 0; ii < langs.length; ++ii ) {
|
|
let langName = langs[ii];
|
|
let isSet = langName == chosenLang;
|
|
langsDiv.appendChild(newRadio(langName, 'lang', isSet,
|
|
function() {chosenLang = langName;}));
|
|
}
|
|
}
|
|
|
|
let dupModeDiv = document.createElement('div');
|
|
dlg.appendChild(dupModeDiv);
|
|
dupModeDiv.appendChild(newCheckbox("Play in Duplicate Mode", 'dupMode', false));
|
|
|
|
const buttons = ['Cancel', 'OK'];
|
|
const butProc = function(indx) {
|
|
if ( buttons[indx] == 'OK' ) {
|
|
let allowHints = document.getElementById('allowHints').checked;
|
|
let dupMode = document.getElementById('dupMode').checked;
|
|
const types = ['number', 'boolean', 'string', 'boolean', 'boolean'];
|
|
const params = [closure, robotSet, chosenLang, allowHints, dupMode];
|
|
Module.ccall('onNewGame', null, types, params);
|
|
}
|
|
dlg.parentNode.removeChild(dlg);
|
|
}
|
|
|
|
dlg.appendChild( newButtonDiv( buttons, butProc, false ) );
|
|
addDepthNote(dlg);
|
|
}
|
|
|
|
for ( let one of ['paho-mqtt.js'] ) {
|
|
let script = document.createElement('script');
|
|
script.src = one;
|
|
document.body.appendChild(script);
|
|
}
|