@mideind/netskrafl-react 1.0.0-beta.8 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/css/netskrafl.css +2 -1
- package/dist/cjs/index.js +901 -827
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +2 -1
- package/dist/esm/index.js +901 -827
- package/dist/esm/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -31,6 +31,27 @@ const DEFAULT_STATE = {
|
|
|
31
31
|
uiLandscape: false,
|
|
32
32
|
runningLocal: false,
|
|
33
33
|
};
|
|
34
|
+
const makeServerUrls = (backendUrl, movesUrl) => {
|
|
35
|
+
// If the last character of the url is a slash, cut it off,
|
|
36
|
+
// since path URLs always start with a slash
|
|
37
|
+
const cleanupUrl = (url) => {
|
|
38
|
+
if (url.length > 0 && url[url.length - 1] === "/") {
|
|
39
|
+
url = url.slice(0, -1);
|
|
40
|
+
}
|
|
41
|
+
return url;
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
serverUrl: cleanupUrl(backendUrl),
|
|
45
|
+
movesUrl: cleanupUrl(movesUrl),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const makeGlobalState = (overrides) => {
|
|
49
|
+
const state = {
|
|
50
|
+
...DEFAULT_STATE,
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
return { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
|
|
54
|
+
};
|
|
34
55
|
|
|
35
56
|
function getDefaultExportFromCjs (x) {
|
|
36
57
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -2187,52 +2208,485 @@ function requireMithril () {
|
|
|
2187
2208
|
var mithrilExports = requireMithril();
|
|
2188
2209
|
var m = /*@__PURE__*/getDefaultExportFromCjs(mithrilExports);
|
|
2189
2210
|
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
let MOVES_ACCESS_KEY = "None";
|
|
2193
|
-
const setServerUrl = (backendUrl, movesUrl, movesAccessKey) => {
|
|
2194
|
-
// If the last character of the url is a slash, cut it off,
|
|
2195
|
-
// since path URLs always start with a slash
|
|
2196
|
-
const cleanupUrl = (url) => {
|
|
2197
|
-
if (url.length > 0 && url[url.length - 1] === "/") {
|
|
2198
|
-
url = url.slice(0, -1);
|
|
2199
|
-
}
|
|
2200
|
-
return url;
|
|
2201
|
-
};
|
|
2202
|
-
if (backendUrl)
|
|
2203
|
-
BACKEND_SERVER_PREFIX = cleanupUrl(backendUrl);
|
|
2204
|
-
if (movesUrl)
|
|
2205
|
-
MOVES_SERVER_PREFIX = cleanupUrl(movesUrl);
|
|
2206
|
-
if (movesAccessKey)
|
|
2207
|
-
MOVES_ACCESS_KEY = movesAccessKey;
|
|
2208
|
-
};
|
|
2209
|
-
const serverUrl = (path) => {
|
|
2210
|
-
return `${BACKEND_SERVER_PREFIX}${path}`;
|
|
2211
|
+
const serverUrl = (state, path) => {
|
|
2212
|
+
return `${state.serverUrl}${path}`;
|
|
2211
2213
|
};
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
+
// Export a non-authenticated request for use by login.ts to avoid circular dependency
|
|
2215
|
+
const requestWithoutAuth = (state, options) => {
|
|
2214
2216
|
return m.request({
|
|
2215
2217
|
withCredentials: true,
|
|
2216
2218
|
...options,
|
|
2217
|
-
url: serverUrl(options.url),
|
|
2219
|
+
url: serverUrl(state, options.url),
|
|
2218
2220
|
});
|
|
2219
2221
|
};
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2222
|
+
|
|
2223
|
+
/*
|
|
2224
|
+
|
|
2225
|
+
i8n.ts
|
|
2226
|
+
|
|
2227
|
+
Single page UI for Netskrafl/Explo using the Mithril library
|
|
2228
|
+
|
|
2229
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2230
|
+
Author: Vilhjálmur Þorsteinsson
|
|
2231
|
+
|
|
2232
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2233
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2234
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2235
|
+
|
|
2236
|
+
|
|
2237
|
+
This module contains internationalization (i18n) utility functions,
|
|
2238
|
+
allowing for translation of displayed text between languages.
|
|
2239
|
+
|
|
2240
|
+
Text messages for individual locales are loaded from the
|
|
2241
|
+
/static/assets/messages.json file, which is fetched from the server.
|
|
2242
|
+
|
|
2243
|
+
*/
|
|
2244
|
+
// Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
|
|
2245
|
+
// This is overwritten in setLocale()
|
|
2246
|
+
let currentLocale = "is_IS";
|
|
2247
|
+
let currentFallback = "is";
|
|
2248
|
+
// Regex that matches embedded interpolations such as "Welcome, {username}!"
|
|
2249
|
+
// Interpolation identifiers should only contain ASCII characters, digits and '_'
|
|
2250
|
+
const rex = /{\s*(\w+)\s*}/g;
|
|
2251
|
+
let messages = {};
|
|
2252
|
+
let messagesLoaded = false;
|
|
2253
|
+
function hasAnyTranslation(msgs, locale) {
|
|
2254
|
+
// Return true if any translation is available for the given locale
|
|
2255
|
+
for (let key in msgs) {
|
|
2256
|
+
if (msgs[key][locale] !== undefined)
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2259
|
+
return false;
|
|
2260
|
+
}
|
|
2261
|
+
function setLocale(locale, msgs) {
|
|
2262
|
+
// Set the current i18n locale and fallback
|
|
2263
|
+
currentLocale = locale;
|
|
2264
|
+
currentFallback = locale.split("_")[0];
|
|
2265
|
+
// For unsupported locales, i.e. locales that have no
|
|
2266
|
+
// translations available for them, fall back to English (U.S.).
|
|
2267
|
+
if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
|
|
2268
|
+
currentLocale = "en_US";
|
|
2269
|
+
currentFallback = "en";
|
|
2270
|
+
}
|
|
2271
|
+
// Flatten the Messages structure, enabling long strings
|
|
2272
|
+
// to be represented as string arrays in the messages.json file
|
|
2273
|
+
messages = {};
|
|
2274
|
+
for (let key in msgs) {
|
|
2275
|
+
for (let lc in msgs[key]) {
|
|
2276
|
+
let s = msgs[key][lc];
|
|
2277
|
+
if (Array.isArray(s))
|
|
2278
|
+
s = s.join("");
|
|
2279
|
+
if (messages[key] === undefined)
|
|
2280
|
+
messages[key] = {};
|
|
2281
|
+
// If the string s contains HTML markup of the form <tag>...</tag>,
|
|
2282
|
+
// convert it into a list of Mithril Vnode children corresponding to
|
|
2283
|
+
// the text and the tags
|
|
2284
|
+
if (s.match(/<[a-z]+>/)) {
|
|
2285
|
+
// Looks like the string contains HTML markup
|
|
2286
|
+
const vnodes = [];
|
|
2287
|
+
let i = 0;
|
|
2288
|
+
let tagMatch = null;
|
|
2289
|
+
while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
|
|
2290
|
+
// Found what looks like an HTML tag
|
|
2291
|
+
// Calculate the index of the enclosed text within s
|
|
2292
|
+
const tag = tagMatch[0];
|
|
2293
|
+
let j = i + tagMatch.index + tag.length;
|
|
2294
|
+
// Find the end tag
|
|
2295
|
+
let end = s.indexOf("</" + tag.slice(1), j);
|
|
2296
|
+
if (end < 0) {
|
|
2297
|
+
// No end tag - skip past this weirdness
|
|
2298
|
+
i = j;
|
|
2299
|
+
continue;
|
|
2300
|
+
}
|
|
2301
|
+
// Add the text preceding the tag
|
|
2302
|
+
if (tagMatch.index > 0)
|
|
2303
|
+
vnodes.push(s.slice(i, i + tagMatch.index));
|
|
2304
|
+
// Create the Mithril node corresponding to the tag and the enclosed text
|
|
2305
|
+
// and add it to the list
|
|
2306
|
+
vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
|
|
2307
|
+
// Advance the index past the end of the tag
|
|
2308
|
+
i = end + tag.length + 1;
|
|
2309
|
+
}
|
|
2310
|
+
// Push the final text part, if any
|
|
2311
|
+
if (i < s.length)
|
|
2312
|
+
vnodes.push(s.slice(i));
|
|
2313
|
+
// Reassign s to the list of vnodes
|
|
2314
|
+
s = vnodes;
|
|
2315
|
+
}
|
|
2316
|
+
messages[key][lc] = s;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
messagesLoaded = true;
|
|
2320
|
+
}
|
|
2321
|
+
async function loadMessages(state, locale) {
|
|
2322
|
+
// Load the internationalization message JSON file from the server
|
|
2323
|
+
// and set the user's locale
|
|
2324
|
+
try {
|
|
2325
|
+
const messages = await requestWithoutAuth(state, {
|
|
2326
|
+
method: "GET",
|
|
2327
|
+
url: "/static/assets/messages.json",
|
|
2328
|
+
withCredentials: false, // Cookies are not allowed for CORS request
|
|
2329
|
+
});
|
|
2330
|
+
setLocale(locale, messages);
|
|
2331
|
+
}
|
|
2332
|
+
catch (_a) {
|
|
2333
|
+
setLocale(locale, {});
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function t(key, ips = {}) {
|
|
2337
|
+
// Main text translation function, supporting interpolation
|
|
2338
|
+
// and HTML tag substitution
|
|
2339
|
+
const msgDict = messages[key];
|
|
2340
|
+
if (msgDict === undefined)
|
|
2341
|
+
// No dictionary for this key - may actually be a missing entry
|
|
2342
|
+
return messagesLoaded ? key : "";
|
|
2343
|
+
// Lookup exact locale, then fallback, then resort to returning the key
|
|
2344
|
+
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
2345
|
+
// If we have an interpolation object, do the interpolation first
|
|
2346
|
+
return Object.keys(ips).length ? interpolate(message, ips) : message;
|
|
2347
|
+
}
|
|
2348
|
+
function ts(key, ips = {}) {
|
|
2349
|
+
// String translation function, supporting interpolation
|
|
2350
|
+
// but not HTML tag substitution
|
|
2351
|
+
const msgDict = messages[key];
|
|
2352
|
+
if (msgDict === undefined)
|
|
2353
|
+
// No dictionary for this key - may actually be a missing entry
|
|
2354
|
+
return messagesLoaded ? key : "";
|
|
2355
|
+
// Lookup exact locale, then fallback, then resort to returning the key
|
|
2356
|
+
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
2357
|
+
if (typeof message != "string")
|
|
2358
|
+
// This is actually an error - the client should be calling t() instead
|
|
2359
|
+
return "";
|
|
2360
|
+
// If we have an interpolation object, do the interpolation first
|
|
2361
|
+
return Object.keys(ips).length ? interpolate_string(message, ips) : message;
|
|
2362
|
+
}
|
|
2363
|
+
function mt(cls, children) {
|
|
2364
|
+
// Wrapper for the Mithril m() function that auto-translates
|
|
2365
|
+
// string and array arguments
|
|
2366
|
+
if (typeof children == "string") {
|
|
2367
|
+
return m(cls, t(children));
|
|
2368
|
+
}
|
|
2369
|
+
if (Array.isArray(children)) {
|
|
2370
|
+
return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
|
|
2371
|
+
}
|
|
2372
|
+
return m(cls, children);
|
|
2373
|
+
}
|
|
2374
|
+
function interpolate(message, ips) {
|
|
2375
|
+
// Replace interpolation placeholders with their corresponding values
|
|
2376
|
+
if (typeof message == "string") {
|
|
2377
|
+
return message.replace(rex, (match, key) => ips[key] || match);
|
|
2378
|
+
}
|
|
2379
|
+
if (Array.isArray(message)) {
|
|
2380
|
+
return message.map((item) => interpolate(item, ips));
|
|
2381
|
+
}
|
|
2382
|
+
return message;
|
|
2383
|
+
}
|
|
2384
|
+
function interpolate_string(message, ips) {
|
|
2385
|
+
// Replace interpolation placeholders with their corresponding values
|
|
2386
|
+
return message.replace(rex, (match, key) => ips[key] || match);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/*
|
|
2390
|
+
|
|
2391
|
+
Types.ts
|
|
2392
|
+
|
|
2393
|
+
Common type definitions for the Explo/Netskrafl user interface
|
|
2394
|
+
|
|
2395
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2396
|
+
Author: Vilhjalmur Thorsteinsson
|
|
2397
|
+
|
|
2398
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2399
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2400
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2401
|
+
|
|
2402
|
+
*/
|
|
2403
|
+
// Global constants
|
|
2404
|
+
const RACK_SIZE = 7;
|
|
2405
|
+
const ROWIDS = "ABCDEFGHIJKLMNO";
|
|
2406
|
+
const BOARD_SIZE = ROWIDS.length;
|
|
2407
|
+
const EXTRA_WIDE_LETTERS = "q";
|
|
2408
|
+
const WIDE_LETTERS = "zxmæ";
|
|
2409
|
+
const ZOOM_FACTOR = 1.5;
|
|
2410
|
+
const ERROR_MESSAGES = {
|
|
2411
|
+
// Translations are found in /static/assets/messages.json
|
|
2412
|
+
1: "Enginn stafur lagður niður",
|
|
2413
|
+
2: "Fyrsta orð verður að liggja um byrjunarreitinn",
|
|
2414
|
+
3: "Orð verður að vera samfellt á borðinu",
|
|
2415
|
+
4: "Orð verður að tengjast orði sem fyrir er",
|
|
2416
|
+
5: "Reitur þegar upptekinn",
|
|
2417
|
+
6: "Ekki má vera eyða í orði",
|
|
2418
|
+
7: "word_not_found",
|
|
2419
|
+
8: "word_not_found",
|
|
2420
|
+
9: "Of margir stafir lagðir niður",
|
|
2421
|
+
10: "Stafur er ekki í rekkanum",
|
|
2422
|
+
11: "Of fáir stafir eftir, skipting ekki leyfð",
|
|
2423
|
+
12: "Of mörgum stöfum skipt",
|
|
2424
|
+
13: "Leik vantar á borðið - notið F5/Refresh",
|
|
2425
|
+
14: "Notandi ekki innskráður - notið F5/Refresh",
|
|
2426
|
+
15: "Rangur eða óþekktur notandi",
|
|
2427
|
+
16: "Viðureign finnst ekki",
|
|
2428
|
+
17: "Viðureign er ekki utan tímamarka",
|
|
2429
|
+
18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
|
|
2430
|
+
19: "Véfenging er ekki möguleg í þessari viðureign",
|
|
2431
|
+
20: "Síðasti leikur er ekki véfengjanlegur",
|
|
2432
|
+
21: "Aðeins véfenging eða pass leyfileg",
|
|
2433
|
+
"server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
|
|
2234
2434
|
};
|
|
2235
2435
|
|
|
2436
|
+
/*
|
|
2437
|
+
|
|
2438
|
+
Util.ts
|
|
2439
|
+
|
|
2440
|
+
Utility functions for the Explo/Netskrafl user interface
|
|
2441
|
+
|
|
2442
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2443
|
+
Author: Vilhjálmur Þorsteinsson
|
|
2444
|
+
|
|
2445
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2446
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2447
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2448
|
+
|
|
2449
|
+
The following code is based on
|
|
2450
|
+
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
|
|
2451
|
+
|
|
2452
|
+
*/
|
|
2453
|
+
// Global vars to cache event state
|
|
2454
|
+
var evCache = [];
|
|
2455
|
+
var origDistance = -1;
|
|
2456
|
+
const PINCH_THRESHOLD = 10; // Minimum pinch movement
|
|
2457
|
+
var hasZoomed = false;
|
|
2458
|
+
// Old-style (non-single-page) game URL prefix
|
|
2459
|
+
const BOARD_PREFIX = "/board?game=";
|
|
2460
|
+
const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
|
|
2461
|
+
function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
|
|
2462
|
+
// Install event handlers for the pointer target
|
|
2463
|
+
attrs.onpointerdown = pointerdown_handler;
|
|
2464
|
+
attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
|
|
2465
|
+
// Use same handler for pointer{up,cancel,out,leave} events since
|
|
2466
|
+
// the semantics for these events - in this app - are the same.
|
|
2467
|
+
attrs.onpointerup = pointerup_handler;
|
|
2468
|
+
attrs.onpointercancel = pointerup_handler;
|
|
2469
|
+
attrs.onpointerout = pointerup_handler;
|
|
2470
|
+
attrs.onpointerleave = pointerup_handler;
|
|
2471
|
+
}
|
|
2472
|
+
function pointerdown_handler(ev) {
|
|
2473
|
+
// The pointerdown event signals the start of a touch interaction.
|
|
2474
|
+
// This event is cached to support 2-finger gestures
|
|
2475
|
+
evCache.push(ev);
|
|
2476
|
+
}
|
|
2477
|
+
function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
|
|
2478
|
+
// This function implements a 2-pointer horizontal pinch/zoom gesture.
|
|
2479
|
+
//
|
|
2480
|
+
// If the distance between the two pointers has increased (zoom in),
|
|
2481
|
+
// the target element's background is changed to "pink" and if the
|
|
2482
|
+
// distance is decreasing (zoom out), the color is changed to "lightblue".
|
|
2483
|
+
//
|
|
2484
|
+
// Find this event in the cache and update its record with this event
|
|
2485
|
+
for (let i = 0; i < evCache.length; i++) {
|
|
2486
|
+
if (ev.pointerId === evCache[i].pointerId) {
|
|
2487
|
+
evCache[i] = ev;
|
|
2488
|
+
break;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
// If two pointers are down, check for pinch gestures
|
|
2492
|
+
if (evCache.length == 2) {
|
|
2493
|
+
// Calculate the distance between the two pointers
|
|
2494
|
+
const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
|
|
2495
|
+
Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
|
|
2496
|
+
if (origDistance > 0) {
|
|
2497
|
+
if (curDistance - origDistance >= PINCH_THRESHOLD) {
|
|
2498
|
+
// The distance between the two pointers has increased
|
|
2499
|
+
if (!hasZoomed)
|
|
2500
|
+
funcZoomIn();
|
|
2501
|
+
hasZoomed = true;
|
|
2502
|
+
}
|
|
2503
|
+
else if (origDistance - curDistance >= PINCH_THRESHOLD) {
|
|
2504
|
+
// The distance between the two pointers has decreased
|
|
2505
|
+
if (!hasZoomed)
|
|
2506
|
+
funcZoomOut();
|
|
2507
|
+
hasZoomed = true;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
else if (origDistance < 0) {
|
|
2511
|
+
// Note the original difference between two pointers
|
|
2512
|
+
origDistance = curDistance;
|
|
2513
|
+
hasZoomed = false;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function pointerup_handler(ev) {
|
|
2518
|
+
// Remove this pointer from the cache and reset the target's
|
|
2519
|
+
// background and border
|
|
2520
|
+
remove_event(ev);
|
|
2521
|
+
// If the number of pointers down is less than two then reset diff tracker
|
|
2522
|
+
if (evCache.length < 2) {
|
|
2523
|
+
origDistance = -1;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function remove_event(ev) {
|
|
2527
|
+
// Remove this event from the target's cache
|
|
2528
|
+
for (let i = 0; i < evCache.length; i++) {
|
|
2529
|
+
if (evCache[i].pointerId === ev.pointerId) {
|
|
2530
|
+
evCache.splice(i, 1);
|
|
2531
|
+
break;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
function buttonOver(ev) {
|
|
2536
|
+
const clist = ev.currentTarget.classList;
|
|
2537
|
+
if (clist !== undefined && !clist.contains("disabled"))
|
|
2538
|
+
clist.add("over");
|
|
2539
|
+
ev.redraw = false;
|
|
2540
|
+
}
|
|
2541
|
+
function buttonOut(ev) {
|
|
2542
|
+
const clist = ev.currentTarget.classList;
|
|
2543
|
+
if (clist !== undefined)
|
|
2544
|
+
clist.remove("over");
|
|
2545
|
+
ev.redraw = false;
|
|
2546
|
+
}
|
|
2547
|
+
// Glyphicon utility function: inserts a glyphicon span
|
|
2548
|
+
function glyph(icon, attrs, grayed) {
|
|
2549
|
+
return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
|
|
2550
|
+
}
|
|
2551
|
+
function glyphGrayed(icon, attrs) {
|
|
2552
|
+
return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
|
|
2553
|
+
}
|
|
2554
|
+
// Utility function: inserts non-breaking space
|
|
2555
|
+
function nbsp(n) {
|
|
2556
|
+
return m.trust(" ");
|
|
2557
|
+
}
|
|
2558
|
+
// Utility functions
|
|
2559
|
+
function escapeHtml(string) {
|
|
2560
|
+
/* Utility function to properly encode a string into HTML */
|
|
2561
|
+
const entityMap = {
|
|
2562
|
+
"&": "&",
|
|
2563
|
+
"<": "<",
|
|
2564
|
+
">": ">",
|
|
2565
|
+
'"': '"',
|
|
2566
|
+
"'": ''',
|
|
2567
|
+
"/": '/'
|
|
2568
|
+
};
|
|
2569
|
+
return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
|
|
2570
|
+
}
|
|
2571
|
+
function getUrlVars(url) {
|
|
2572
|
+
// Get values from a URL query string
|
|
2573
|
+
const hashes = url.split('&');
|
|
2574
|
+
const vars = {};
|
|
2575
|
+
for (let i = 0; i < hashes.length; i++) {
|
|
2576
|
+
const hash = hashes[i].split('=');
|
|
2577
|
+
if (hash.length == 2)
|
|
2578
|
+
vars[hash[0]] = decodeURIComponent(hash[1]);
|
|
2579
|
+
}
|
|
2580
|
+
return vars;
|
|
2581
|
+
}
|
|
2582
|
+
function getInput(id) {
|
|
2583
|
+
// Return the current value of a text input field
|
|
2584
|
+
const elem = document.getElementById(id);
|
|
2585
|
+
return elem.value;
|
|
2586
|
+
}
|
|
2587
|
+
function setInput(id, val) {
|
|
2588
|
+
// Set the current value of a text input field
|
|
2589
|
+
const elem = document.getElementById(id);
|
|
2590
|
+
elem.value = val;
|
|
2591
|
+
}
|
|
2592
|
+
function playAudio(elemId) {
|
|
2593
|
+
// Play an audio file
|
|
2594
|
+
const sound = document.getElementById(elemId);
|
|
2595
|
+
if (sound)
|
|
2596
|
+
sound.play();
|
|
2597
|
+
}
|
|
2598
|
+
function arrayEqual(a, b) {
|
|
2599
|
+
// Return true if arrays a and b are equal
|
|
2600
|
+
if (a.length != b.length)
|
|
2601
|
+
return false;
|
|
2602
|
+
for (let i = 0; i < a.length; i++)
|
|
2603
|
+
if (a[i] != b[i])
|
|
2604
|
+
return false;
|
|
2605
|
+
return true;
|
|
2606
|
+
}
|
|
2607
|
+
function gameUrl(url) {
|
|
2608
|
+
// Convert old-style game URL to new-style single-page URL
|
|
2609
|
+
// The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
|
|
2610
|
+
if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
|
|
2611
|
+
// Cut off "/board?game="
|
|
2612
|
+
url = url.slice(BOARD_PREFIX_LEN);
|
|
2613
|
+
// Isolate the game UUID
|
|
2614
|
+
const uuid = url.slice(0, 36);
|
|
2615
|
+
// Isolate the other parameters, if any
|
|
2616
|
+
let params = url.slice(36);
|
|
2617
|
+
// Start parameter section of URL with a ? sign
|
|
2618
|
+
if (params.length > 0 && params.charAt(0) == "&")
|
|
2619
|
+
params = "?" + params.slice(1);
|
|
2620
|
+
// Return the single-page URL, to be consumed by m.route.Link()
|
|
2621
|
+
return "/game/" + uuid + params;
|
|
2622
|
+
}
|
|
2623
|
+
function scrollMovelistToBottom() {
|
|
2624
|
+
// If the length of the move list has changed,
|
|
2625
|
+
// scroll the last move into view
|
|
2626
|
+
let movelist = document.querySelectorAll("div.movelist .move");
|
|
2627
|
+
if (!movelist || !movelist.length)
|
|
2628
|
+
return;
|
|
2629
|
+
let target = movelist[movelist.length - 1];
|
|
2630
|
+
let parent = target.parentNode;
|
|
2631
|
+
let len = parent.getAttribute("data-len");
|
|
2632
|
+
let intLen = (!len) ? 0 : parseInt(len);
|
|
2633
|
+
if (movelist.length > intLen) {
|
|
2634
|
+
// The list has grown since we last updated it:
|
|
2635
|
+
// scroll to the bottom and mark its length
|
|
2636
|
+
parent.scrollTop = target.offsetTop;
|
|
2637
|
+
}
|
|
2638
|
+
parent.setAttribute("data-len", movelist.length.toString());
|
|
2639
|
+
}
|
|
2640
|
+
function coord(row, col, vertical = false) {
|
|
2641
|
+
// Return the co-ordinate string for the given 0-based row and col
|
|
2642
|
+
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
|
|
2643
|
+
return null;
|
|
2644
|
+
// Horizontal moves have the row letter first
|
|
2645
|
+
// Vertical moves have the column number first
|
|
2646
|
+
return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
|
|
2647
|
+
}
|
|
2648
|
+
function toVector(co) {
|
|
2649
|
+
// Convert a co-ordinate string to a 0-based row, col and direction vector
|
|
2650
|
+
var dx = 0, dy = 0;
|
|
2651
|
+
var col = 0;
|
|
2652
|
+
var row = ROWIDS.indexOf(co[0]);
|
|
2653
|
+
if (row >= 0) {
|
|
2654
|
+
/* Horizontal move */
|
|
2655
|
+
col = parseInt(co.slice(1)) - 1;
|
|
2656
|
+
dx = 1;
|
|
2657
|
+
}
|
|
2658
|
+
else {
|
|
2659
|
+
/* Vertical move */
|
|
2660
|
+
row = ROWIDS.indexOf(co.slice(-1));
|
|
2661
|
+
col = parseInt(co) - 1;
|
|
2662
|
+
dy = 1;
|
|
2663
|
+
}
|
|
2664
|
+
return { col: col, row: row, dx: dx, dy: dy };
|
|
2665
|
+
}
|
|
2666
|
+
function valueOrK(value, breakpoint = 10000) {
|
|
2667
|
+
// Return a numeric value as a string, but in kilos (thousands)
|
|
2668
|
+
// if it exceeds a breakpoint, in that case suffixed by "K"
|
|
2669
|
+
const sign = value < 0 ? "-" : "";
|
|
2670
|
+
value = Math.abs(value);
|
|
2671
|
+
if (value < breakpoint)
|
|
2672
|
+
return `${sign}${value}`;
|
|
2673
|
+
value = Math.round(value / 1000);
|
|
2674
|
+
return `${sign}${value}K`;
|
|
2675
|
+
}
|
|
2676
|
+
// SalesCloud stuff
|
|
2677
|
+
function doRegisterSalesCloud(i, s, o, g, r, a, m) {
|
|
2678
|
+
i.SalesCloudObject = r;
|
|
2679
|
+
i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
|
|
2680
|
+
i[r].l = 1 * new Date();
|
|
2681
|
+
a = s.createElement(o);
|
|
2682
|
+
m = s.getElementsByTagName(o)[0];
|
|
2683
|
+
a.src = g;
|
|
2684
|
+
m.parentNode.insertBefore(a, m);
|
|
2685
|
+
}
|
|
2686
|
+
function registerSalesCloud() {
|
|
2687
|
+
doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2236
2690
|
/**
|
|
2237
2691
|
* @license
|
|
2238
2692
|
* Copyright 2017 Google LLC
|
|
@@ -26558,33 +27012,46 @@ let database;
|
|
|
26558
27012
|
let analytics;
|
|
26559
27013
|
function initFirebase(state) {
|
|
26560
27014
|
try {
|
|
26561
|
-
const projectId = state
|
|
27015
|
+
const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
|
|
26562
27016
|
const firebaseOptions = {
|
|
26563
27017
|
projectId,
|
|
26564
|
-
apiKey:
|
|
27018
|
+
apiKey: firebaseAPIKey,
|
|
26565
27019
|
authDomain: `${projectId}.firebaseapp.com`,
|
|
26566
|
-
databaseURL
|
|
27020
|
+
databaseURL,
|
|
26567
27021
|
storageBucket: `${projectId}.firebasestorage.app`,
|
|
26568
|
-
messagingSenderId:
|
|
26569
|
-
appId:
|
|
26570
|
-
measurementId
|
|
27022
|
+
messagingSenderId: firebaseSenderId,
|
|
27023
|
+
appId: firebaseAppId,
|
|
27024
|
+
measurementId,
|
|
26571
27025
|
};
|
|
26572
|
-
app = initializeApp(firebaseOptions,
|
|
27026
|
+
app = initializeApp(firebaseOptions, projectId);
|
|
26573
27027
|
if (!app) {
|
|
26574
27028
|
console.error("Failed to initialize Firebase");
|
|
27029
|
+
return false;
|
|
26575
27030
|
}
|
|
26576
27031
|
}
|
|
26577
27032
|
catch (e) {
|
|
26578
27033
|
console.error("Failed to initialize Firebase", e);
|
|
27034
|
+
return false;
|
|
27035
|
+
}
|
|
27036
|
+
return true;
|
|
27037
|
+
}
|
|
27038
|
+
function isFirebaseAuthenticated(state) {
|
|
27039
|
+
// Check if Firebase is currently authenticated
|
|
27040
|
+
// If auth is not initialized but state is provided, try to initialize
|
|
27041
|
+
if (!auth) {
|
|
27042
|
+
if (!app)
|
|
27043
|
+
initFirebase(state);
|
|
27044
|
+
if (app)
|
|
27045
|
+
auth = getAuth(app);
|
|
26579
27046
|
}
|
|
27047
|
+
if (!auth)
|
|
27048
|
+
return false;
|
|
27049
|
+
return auth.currentUser !== null;
|
|
26580
27050
|
}
|
|
26581
27051
|
async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
26582
|
-
if (!app)
|
|
26583
|
-
initFirebase(state);
|
|
26584
|
-
if (!app)
|
|
27052
|
+
if (!app && !initFirebase(state))
|
|
26585
27053
|
return;
|
|
26586
|
-
const userId = state
|
|
26587
|
-
const locale = state.locale;
|
|
27054
|
+
const { userId, locale, projectId, loginMethod } = state;
|
|
26588
27055
|
auth = getAuth(app);
|
|
26589
27056
|
if (!auth) {
|
|
26590
27057
|
console.error("Failed to initialize Firebase Auth");
|
|
@@ -26597,16 +27064,16 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
|
26597
27064
|
// For new users, log an additional signup event
|
|
26598
27065
|
if (state.newUser) {
|
|
26599
27066
|
logEvent("sign_up", {
|
|
26600
|
-
locale
|
|
26601
|
-
method:
|
|
26602
|
-
userid:
|
|
27067
|
+
locale,
|
|
27068
|
+
method: loginMethod,
|
|
27069
|
+
userid: userId
|
|
26603
27070
|
});
|
|
26604
27071
|
}
|
|
26605
27072
|
// And always log a login event
|
|
26606
27073
|
logEvent("login", {
|
|
26607
|
-
locale
|
|
26608
|
-
method:
|
|
26609
|
-
userid:
|
|
27074
|
+
locale,
|
|
27075
|
+
method: loginMethod,
|
|
27076
|
+
userid: userId
|
|
26610
27077
|
});
|
|
26611
27078
|
}
|
|
26612
27079
|
});
|
|
@@ -26621,7 +27088,7 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
|
26621
27088
|
if (!analytics) {
|
|
26622
27089
|
console.error("Failed to initialize Firebase Analytics");
|
|
26623
27090
|
}
|
|
26624
|
-
initPresence(
|
|
27091
|
+
initPresence(projectId, userId, locale);
|
|
26625
27092
|
}
|
|
26626
27093
|
function initPresence(projectId, userId, locale) {
|
|
26627
27094
|
// Ensure that this user connection is recorded in Firebase
|
|
@@ -26678,473 +27145,6 @@ function logEvent(ev, params) {
|
|
|
26678
27145
|
logEvent$2(analytics, ev, params);
|
|
26679
27146
|
}
|
|
26680
27147
|
|
|
26681
|
-
/*
|
|
26682
|
-
|
|
26683
|
-
i8n.ts
|
|
26684
|
-
|
|
26685
|
-
Single page UI for Netskrafl/Explo using the Mithril library
|
|
26686
|
-
|
|
26687
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26688
|
-
Author: Vilhjálmur Þorsteinsson
|
|
26689
|
-
|
|
26690
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26691
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26692
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26693
|
-
|
|
26694
|
-
|
|
26695
|
-
This module contains internationalization (i18n) utility functions,
|
|
26696
|
-
allowing for translation of displayed text between languages.
|
|
26697
|
-
|
|
26698
|
-
Text messages for individual locales are loaded from the
|
|
26699
|
-
/static/assets/messages.json file, which is fetched from the server.
|
|
26700
|
-
|
|
26701
|
-
*/
|
|
26702
|
-
// Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
|
|
26703
|
-
// This is overwritten in setLocale()
|
|
26704
|
-
let currentLocale = "is_IS";
|
|
26705
|
-
let currentFallback = "is";
|
|
26706
|
-
// Regex that matches embedded interpolations such as "Welcome, {username}!"
|
|
26707
|
-
// Interpolation identifiers should only contain ASCII characters, digits and '_'
|
|
26708
|
-
const rex = /{\s*(\w+)\s*}/g;
|
|
26709
|
-
let messages = {};
|
|
26710
|
-
let messagesLoaded = false;
|
|
26711
|
-
function hasAnyTranslation(msgs, locale) {
|
|
26712
|
-
// Return true if any translation is available for the given locale
|
|
26713
|
-
for (let key in msgs) {
|
|
26714
|
-
if (msgs[key][locale] !== undefined)
|
|
26715
|
-
return true;
|
|
26716
|
-
}
|
|
26717
|
-
return false;
|
|
26718
|
-
}
|
|
26719
|
-
function setLocale(locale, msgs) {
|
|
26720
|
-
// Set the current i18n locale and fallback
|
|
26721
|
-
currentLocale = locale;
|
|
26722
|
-
currentFallback = locale.split("_")[0];
|
|
26723
|
-
// For unsupported locales, i.e. locales that have no
|
|
26724
|
-
// translations available for them, fall back to English (U.S.).
|
|
26725
|
-
if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
|
|
26726
|
-
currentLocale = "en_US";
|
|
26727
|
-
currentFallback = "en";
|
|
26728
|
-
}
|
|
26729
|
-
// Flatten the Messages structure, enabling long strings
|
|
26730
|
-
// to be represented as string arrays in the messages.json file
|
|
26731
|
-
messages = {};
|
|
26732
|
-
for (let key in msgs) {
|
|
26733
|
-
for (let lc in msgs[key]) {
|
|
26734
|
-
let s = msgs[key][lc];
|
|
26735
|
-
if (Array.isArray(s))
|
|
26736
|
-
s = s.join("");
|
|
26737
|
-
if (messages[key] === undefined)
|
|
26738
|
-
messages[key] = {};
|
|
26739
|
-
// If the string s contains HTML markup of the form <tag>...</tag>,
|
|
26740
|
-
// convert it into a list of Mithril Vnode children corresponding to
|
|
26741
|
-
// the text and the tags
|
|
26742
|
-
if (s.match(/<[a-z]+>/)) {
|
|
26743
|
-
// Looks like the string contains HTML markup
|
|
26744
|
-
const vnodes = [];
|
|
26745
|
-
let i = 0;
|
|
26746
|
-
let tagMatch = null;
|
|
26747
|
-
while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
|
|
26748
|
-
// Found what looks like an HTML tag
|
|
26749
|
-
// Calculate the index of the enclosed text within s
|
|
26750
|
-
const tag = tagMatch[0];
|
|
26751
|
-
let j = i + tagMatch.index + tag.length;
|
|
26752
|
-
// Find the end tag
|
|
26753
|
-
let end = s.indexOf("</" + tag.slice(1), j);
|
|
26754
|
-
if (end < 0) {
|
|
26755
|
-
// No end tag - skip past this weirdness
|
|
26756
|
-
i = j;
|
|
26757
|
-
continue;
|
|
26758
|
-
}
|
|
26759
|
-
// Add the text preceding the tag
|
|
26760
|
-
if (tagMatch.index > 0)
|
|
26761
|
-
vnodes.push(s.slice(i, i + tagMatch.index));
|
|
26762
|
-
// Create the Mithril node corresponding to the tag and the enclosed text
|
|
26763
|
-
// and add it to the list
|
|
26764
|
-
vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
|
|
26765
|
-
// Advance the index past the end of the tag
|
|
26766
|
-
i = end + tag.length + 1;
|
|
26767
|
-
}
|
|
26768
|
-
// Push the final text part, if any
|
|
26769
|
-
if (i < s.length)
|
|
26770
|
-
vnodes.push(s.slice(i));
|
|
26771
|
-
// Reassign s to the list of vnodes
|
|
26772
|
-
s = vnodes;
|
|
26773
|
-
}
|
|
26774
|
-
messages[key][lc] = s;
|
|
26775
|
-
}
|
|
26776
|
-
}
|
|
26777
|
-
messagesLoaded = true;
|
|
26778
|
-
}
|
|
26779
|
-
async function loadMessages(locale) {
|
|
26780
|
-
// Load the internationalization message JSON file from the server
|
|
26781
|
-
// and set the user's locale
|
|
26782
|
-
try {
|
|
26783
|
-
const messages = await request({
|
|
26784
|
-
method: "GET",
|
|
26785
|
-
url: "/static/assets/messages.json",
|
|
26786
|
-
withCredentials: false, // Cookies are not allowed for CORS request
|
|
26787
|
-
});
|
|
26788
|
-
setLocale(locale, messages);
|
|
26789
|
-
}
|
|
26790
|
-
catch (_a) {
|
|
26791
|
-
setLocale(locale, {});
|
|
26792
|
-
}
|
|
26793
|
-
}
|
|
26794
|
-
function t(key, ips = {}) {
|
|
26795
|
-
// Main text translation function, supporting interpolation
|
|
26796
|
-
// and HTML tag substitution
|
|
26797
|
-
const msgDict = messages[key];
|
|
26798
|
-
if (msgDict === undefined)
|
|
26799
|
-
// No dictionary for this key - may actually be a missing entry
|
|
26800
|
-
return messagesLoaded ? key : "";
|
|
26801
|
-
// Lookup exact locale, then fallback, then resort to returning the key
|
|
26802
|
-
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
26803
|
-
// If we have an interpolation object, do the interpolation first
|
|
26804
|
-
return Object.keys(ips).length ? interpolate(message, ips) : message;
|
|
26805
|
-
}
|
|
26806
|
-
function ts(key, ips = {}) {
|
|
26807
|
-
// String translation function, supporting interpolation
|
|
26808
|
-
// but not HTML tag substitution
|
|
26809
|
-
const msgDict = messages[key];
|
|
26810
|
-
if (msgDict === undefined)
|
|
26811
|
-
// No dictionary for this key - may actually be a missing entry
|
|
26812
|
-
return messagesLoaded ? key : "";
|
|
26813
|
-
// Lookup exact locale, then fallback, then resort to returning the key
|
|
26814
|
-
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
26815
|
-
if (typeof message != "string")
|
|
26816
|
-
// This is actually an error - the client should be calling t() instead
|
|
26817
|
-
return "";
|
|
26818
|
-
// If we have an interpolation object, do the interpolation first
|
|
26819
|
-
return Object.keys(ips).length ? interpolate_string(message, ips) : message;
|
|
26820
|
-
}
|
|
26821
|
-
function mt(cls, children) {
|
|
26822
|
-
// Wrapper for the Mithril m() function that auto-translates
|
|
26823
|
-
// string and array arguments
|
|
26824
|
-
if (typeof children == "string") {
|
|
26825
|
-
return m(cls, t(children));
|
|
26826
|
-
}
|
|
26827
|
-
if (Array.isArray(children)) {
|
|
26828
|
-
return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
|
|
26829
|
-
}
|
|
26830
|
-
return m(cls, children);
|
|
26831
|
-
}
|
|
26832
|
-
function interpolate(message, ips) {
|
|
26833
|
-
// Replace interpolation placeholders with their corresponding values
|
|
26834
|
-
if (typeof message == "string") {
|
|
26835
|
-
return message.replace(rex, (match, key) => ips[key] || match);
|
|
26836
|
-
}
|
|
26837
|
-
if (Array.isArray(message)) {
|
|
26838
|
-
return message.map((item) => interpolate(item, ips));
|
|
26839
|
-
}
|
|
26840
|
-
return message;
|
|
26841
|
-
}
|
|
26842
|
-
function interpolate_string(message, ips) {
|
|
26843
|
-
// Replace interpolation placeholders with their corresponding values
|
|
26844
|
-
return message.replace(rex, (match, key) => ips[key] || match);
|
|
26845
|
-
}
|
|
26846
|
-
|
|
26847
|
-
/*
|
|
26848
|
-
|
|
26849
|
-
Types.ts
|
|
26850
|
-
|
|
26851
|
-
Common type definitions for the Explo/Netskrafl user interface
|
|
26852
|
-
|
|
26853
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26854
|
-
Author: Vilhjalmur Thorsteinsson
|
|
26855
|
-
|
|
26856
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26857
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26858
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26859
|
-
|
|
26860
|
-
*/
|
|
26861
|
-
// Global constants
|
|
26862
|
-
const RACK_SIZE = 7;
|
|
26863
|
-
const ROWIDS = "ABCDEFGHIJKLMNO";
|
|
26864
|
-
const BOARD_SIZE = ROWIDS.length;
|
|
26865
|
-
const EXTRA_WIDE_LETTERS = "q";
|
|
26866
|
-
const WIDE_LETTERS = "zxmæ";
|
|
26867
|
-
const ZOOM_FACTOR = 1.5;
|
|
26868
|
-
const ERROR_MESSAGES = {
|
|
26869
|
-
// Translations are found in /static/assets/messages.json
|
|
26870
|
-
1: "Enginn stafur lagður niður",
|
|
26871
|
-
2: "Fyrsta orð verður að liggja um byrjunarreitinn",
|
|
26872
|
-
3: "Orð verður að vera samfellt á borðinu",
|
|
26873
|
-
4: "Orð verður að tengjast orði sem fyrir er",
|
|
26874
|
-
5: "Reitur þegar upptekinn",
|
|
26875
|
-
6: "Ekki má vera eyða í orði",
|
|
26876
|
-
7: "word_not_found",
|
|
26877
|
-
8: "word_not_found",
|
|
26878
|
-
9: "Of margir stafir lagðir niður",
|
|
26879
|
-
10: "Stafur er ekki í rekkanum",
|
|
26880
|
-
11: "Of fáir stafir eftir, skipting ekki leyfð",
|
|
26881
|
-
12: "Of mörgum stöfum skipt",
|
|
26882
|
-
13: "Leik vantar á borðið - notið F5/Refresh",
|
|
26883
|
-
14: "Notandi ekki innskráður - notið F5/Refresh",
|
|
26884
|
-
15: "Rangur eða óþekktur notandi",
|
|
26885
|
-
16: "Viðureign finnst ekki",
|
|
26886
|
-
17: "Viðureign er ekki utan tímamarka",
|
|
26887
|
-
18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
|
|
26888
|
-
19: "Véfenging er ekki möguleg í þessari viðureign",
|
|
26889
|
-
20: "Síðasti leikur er ekki véfengjanlegur",
|
|
26890
|
-
21: "Aðeins véfenging eða pass leyfileg",
|
|
26891
|
-
"server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
|
|
26892
|
-
};
|
|
26893
|
-
|
|
26894
|
-
/*
|
|
26895
|
-
|
|
26896
|
-
Util.ts
|
|
26897
|
-
|
|
26898
|
-
Utility functions for the Explo/Netskrafl user interface
|
|
26899
|
-
|
|
26900
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26901
|
-
Author: Vilhjálmur Þorsteinsson
|
|
26902
|
-
|
|
26903
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26904
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26905
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26906
|
-
|
|
26907
|
-
The following code is based on
|
|
26908
|
-
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
|
|
26909
|
-
|
|
26910
|
-
*/
|
|
26911
|
-
// Global vars to cache event state
|
|
26912
|
-
var evCache = [];
|
|
26913
|
-
var origDistance = -1;
|
|
26914
|
-
const PINCH_THRESHOLD = 10; // Minimum pinch movement
|
|
26915
|
-
var hasZoomed = false;
|
|
26916
|
-
// Old-style (non-single-page) game URL prefix
|
|
26917
|
-
const BOARD_PREFIX = "/board?game=";
|
|
26918
|
-
const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
|
|
26919
|
-
function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
|
|
26920
|
-
// Install event handlers for the pointer target
|
|
26921
|
-
attrs.onpointerdown = pointerdown_handler;
|
|
26922
|
-
attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
|
|
26923
|
-
// Use same handler for pointer{up,cancel,out,leave} events since
|
|
26924
|
-
// the semantics for these events - in this app - are the same.
|
|
26925
|
-
attrs.onpointerup = pointerup_handler;
|
|
26926
|
-
attrs.onpointercancel = pointerup_handler;
|
|
26927
|
-
attrs.onpointerout = pointerup_handler;
|
|
26928
|
-
attrs.onpointerleave = pointerup_handler;
|
|
26929
|
-
}
|
|
26930
|
-
function pointerdown_handler(ev) {
|
|
26931
|
-
// The pointerdown event signals the start of a touch interaction.
|
|
26932
|
-
// This event is cached to support 2-finger gestures
|
|
26933
|
-
evCache.push(ev);
|
|
26934
|
-
}
|
|
26935
|
-
function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
|
|
26936
|
-
// This function implements a 2-pointer horizontal pinch/zoom gesture.
|
|
26937
|
-
//
|
|
26938
|
-
// If the distance between the two pointers has increased (zoom in),
|
|
26939
|
-
// the target element's background is changed to "pink" and if the
|
|
26940
|
-
// distance is decreasing (zoom out), the color is changed to "lightblue".
|
|
26941
|
-
//
|
|
26942
|
-
// Find this event in the cache and update its record with this event
|
|
26943
|
-
for (let i = 0; i < evCache.length; i++) {
|
|
26944
|
-
if (ev.pointerId === evCache[i].pointerId) {
|
|
26945
|
-
evCache[i] = ev;
|
|
26946
|
-
break;
|
|
26947
|
-
}
|
|
26948
|
-
}
|
|
26949
|
-
// If two pointers are down, check for pinch gestures
|
|
26950
|
-
if (evCache.length == 2) {
|
|
26951
|
-
// Calculate the distance between the two pointers
|
|
26952
|
-
const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
|
|
26953
|
-
Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
|
|
26954
|
-
if (origDistance > 0) {
|
|
26955
|
-
if (curDistance - origDistance >= PINCH_THRESHOLD) {
|
|
26956
|
-
// The distance between the two pointers has increased
|
|
26957
|
-
if (!hasZoomed)
|
|
26958
|
-
funcZoomIn();
|
|
26959
|
-
hasZoomed = true;
|
|
26960
|
-
}
|
|
26961
|
-
else if (origDistance - curDistance >= PINCH_THRESHOLD) {
|
|
26962
|
-
// The distance between the two pointers has decreased
|
|
26963
|
-
if (!hasZoomed)
|
|
26964
|
-
funcZoomOut();
|
|
26965
|
-
hasZoomed = true;
|
|
26966
|
-
}
|
|
26967
|
-
}
|
|
26968
|
-
else if (origDistance < 0) {
|
|
26969
|
-
// Note the original difference between two pointers
|
|
26970
|
-
origDistance = curDistance;
|
|
26971
|
-
hasZoomed = false;
|
|
26972
|
-
}
|
|
26973
|
-
}
|
|
26974
|
-
}
|
|
26975
|
-
function pointerup_handler(ev) {
|
|
26976
|
-
// Remove this pointer from the cache and reset the target's
|
|
26977
|
-
// background and border
|
|
26978
|
-
remove_event(ev);
|
|
26979
|
-
// If the number of pointers down is less than two then reset diff tracker
|
|
26980
|
-
if (evCache.length < 2) {
|
|
26981
|
-
origDistance = -1;
|
|
26982
|
-
}
|
|
26983
|
-
}
|
|
26984
|
-
function remove_event(ev) {
|
|
26985
|
-
// Remove this event from the target's cache
|
|
26986
|
-
for (let i = 0; i < evCache.length; i++) {
|
|
26987
|
-
if (evCache[i].pointerId === ev.pointerId) {
|
|
26988
|
-
evCache.splice(i, 1);
|
|
26989
|
-
break;
|
|
26990
|
-
}
|
|
26991
|
-
}
|
|
26992
|
-
}
|
|
26993
|
-
function buttonOver(ev) {
|
|
26994
|
-
const clist = ev.currentTarget.classList;
|
|
26995
|
-
if (clist !== undefined && !clist.contains("disabled"))
|
|
26996
|
-
clist.add("over");
|
|
26997
|
-
ev.redraw = false;
|
|
26998
|
-
}
|
|
26999
|
-
function buttonOut(ev) {
|
|
27000
|
-
const clist = ev.currentTarget.classList;
|
|
27001
|
-
if (clist !== undefined)
|
|
27002
|
-
clist.remove("over");
|
|
27003
|
-
ev.redraw = false;
|
|
27004
|
-
}
|
|
27005
|
-
// Glyphicon utility function: inserts a glyphicon span
|
|
27006
|
-
function glyph(icon, attrs, grayed) {
|
|
27007
|
-
return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
|
|
27008
|
-
}
|
|
27009
|
-
function glyphGrayed(icon, attrs) {
|
|
27010
|
-
return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
|
|
27011
|
-
}
|
|
27012
|
-
// Utility function: inserts non-breaking space
|
|
27013
|
-
function nbsp(n) {
|
|
27014
|
-
return m.trust(" ");
|
|
27015
|
-
}
|
|
27016
|
-
// Utility functions
|
|
27017
|
-
function escapeHtml(string) {
|
|
27018
|
-
/* Utility function to properly encode a string into HTML */
|
|
27019
|
-
const entityMap = {
|
|
27020
|
-
"&": "&",
|
|
27021
|
-
"<": "<",
|
|
27022
|
-
">": ">",
|
|
27023
|
-
'"': '"',
|
|
27024
|
-
"'": ''',
|
|
27025
|
-
"/": '/'
|
|
27026
|
-
};
|
|
27027
|
-
return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
|
|
27028
|
-
}
|
|
27029
|
-
function getUrlVars(url) {
|
|
27030
|
-
// Get values from a URL query string
|
|
27031
|
-
const hashes = url.split('&');
|
|
27032
|
-
const vars = {};
|
|
27033
|
-
for (let i = 0; i < hashes.length; i++) {
|
|
27034
|
-
const hash = hashes[i].split('=');
|
|
27035
|
-
if (hash.length == 2)
|
|
27036
|
-
vars[hash[0]] = decodeURIComponent(hash[1]);
|
|
27037
|
-
}
|
|
27038
|
-
return vars;
|
|
27039
|
-
}
|
|
27040
|
-
function getInput(id) {
|
|
27041
|
-
// Return the current value of a text input field
|
|
27042
|
-
const elem = document.getElementById(id);
|
|
27043
|
-
return elem.value;
|
|
27044
|
-
}
|
|
27045
|
-
function setInput(id, val) {
|
|
27046
|
-
// Set the current value of a text input field
|
|
27047
|
-
const elem = document.getElementById(id);
|
|
27048
|
-
elem.value = val;
|
|
27049
|
-
}
|
|
27050
|
-
function playAudio(elemId) {
|
|
27051
|
-
// Play an audio file
|
|
27052
|
-
const sound = document.getElementById(elemId);
|
|
27053
|
-
if (sound)
|
|
27054
|
-
sound.play();
|
|
27055
|
-
}
|
|
27056
|
-
function arrayEqual(a, b) {
|
|
27057
|
-
// Return true if arrays a and b are equal
|
|
27058
|
-
if (a.length != b.length)
|
|
27059
|
-
return false;
|
|
27060
|
-
for (let i = 0; i < a.length; i++)
|
|
27061
|
-
if (a[i] != b[i])
|
|
27062
|
-
return false;
|
|
27063
|
-
return true;
|
|
27064
|
-
}
|
|
27065
|
-
function gameUrl(url) {
|
|
27066
|
-
// Convert old-style game URL to new-style single-page URL
|
|
27067
|
-
// The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
|
|
27068
|
-
if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
|
|
27069
|
-
// Cut off "/board?game="
|
|
27070
|
-
url = url.slice(BOARD_PREFIX_LEN);
|
|
27071
|
-
// Isolate the game UUID
|
|
27072
|
-
const uuid = url.slice(0, 36);
|
|
27073
|
-
// Isolate the other parameters, if any
|
|
27074
|
-
let params = url.slice(36);
|
|
27075
|
-
// Start parameter section of URL with a ? sign
|
|
27076
|
-
if (params.length > 0 && params.charAt(0) == "&")
|
|
27077
|
-
params = "?" + params.slice(1);
|
|
27078
|
-
// Return the single-page URL, to be consumed by m.route.Link()
|
|
27079
|
-
return "/game/" + uuid + params;
|
|
27080
|
-
}
|
|
27081
|
-
function scrollMovelistToBottom() {
|
|
27082
|
-
// If the length of the move list has changed,
|
|
27083
|
-
// scroll the last move into view
|
|
27084
|
-
let movelist = document.querySelectorAll("div.movelist .move");
|
|
27085
|
-
if (!movelist || !movelist.length)
|
|
27086
|
-
return;
|
|
27087
|
-
let target = movelist[movelist.length - 1];
|
|
27088
|
-
let parent = target.parentNode;
|
|
27089
|
-
let len = parent.getAttribute("data-len");
|
|
27090
|
-
let intLen = (!len) ? 0 : parseInt(len);
|
|
27091
|
-
if (movelist.length > intLen) {
|
|
27092
|
-
// The list has grown since we last updated it:
|
|
27093
|
-
// scroll to the bottom and mark its length
|
|
27094
|
-
parent.scrollTop = target.offsetTop;
|
|
27095
|
-
}
|
|
27096
|
-
parent.setAttribute("data-len", movelist.length.toString());
|
|
27097
|
-
}
|
|
27098
|
-
function coord(row, col, vertical = false) {
|
|
27099
|
-
// Return the co-ordinate string for the given 0-based row and col
|
|
27100
|
-
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
|
|
27101
|
-
return null;
|
|
27102
|
-
// Horizontal moves have the row letter first
|
|
27103
|
-
// Vertical moves have the column number first
|
|
27104
|
-
return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
|
|
27105
|
-
}
|
|
27106
|
-
function toVector(co) {
|
|
27107
|
-
// Convert a co-ordinate string to a 0-based row, col and direction vector
|
|
27108
|
-
var dx = 0, dy = 0;
|
|
27109
|
-
var col = 0;
|
|
27110
|
-
var row = ROWIDS.indexOf(co[0]);
|
|
27111
|
-
if (row >= 0) {
|
|
27112
|
-
/* Horizontal move */
|
|
27113
|
-
col = parseInt(co.slice(1)) - 1;
|
|
27114
|
-
dx = 1;
|
|
27115
|
-
}
|
|
27116
|
-
else {
|
|
27117
|
-
/* Vertical move */
|
|
27118
|
-
row = ROWIDS.indexOf(co.slice(-1));
|
|
27119
|
-
col = parseInt(co) - 1;
|
|
27120
|
-
dy = 1;
|
|
27121
|
-
}
|
|
27122
|
-
return { col: col, row: row, dx: dx, dy: dy };
|
|
27123
|
-
}
|
|
27124
|
-
function valueOrK(value, breakpoint = 10000) {
|
|
27125
|
-
// Return a numeric value as a string, but in kilos (thousands)
|
|
27126
|
-
// if it exceeds a breakpoint, in that case suffixed by "K"
|
|
27127
|
-
const sign = value < 0 ? "-" : "";
|
|
27128
|
-
value = Math.abs(value);
|
|
27129
|
-
if (value < breakpoint)
|
|
27130
|
-
return `${sign}${value}`;
|
|
27131
|
-
value = Math.round(value / 1000);
|
|
27132
|
-
return `${sign}${value}K`;
|
|
27133
|
-
}
|
|
27134
|
-
// SalesCloud stuff
|
|
27135
|
-
function doRegisterSalesCloud(i, s, o, g, r, a, m) {
|
|
27136
|
-
i.SalesCloudObject = r;
|
|
27137
|
-
i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
|
|
27138
|
-
i[r].l = 1 * new Date();
|
|
27139
|
-
a = s.createElement(o);
|
|
27140
|
-
m = s.getElementsByTagName(o)[0];
|
|
27141
|
-
a.src = g;
|
|
27142
|
-
m.parentNode.insertBefore(a, m);
|
|
27143
|
-
}
|
|
27144
|
-
function registerSalesCloud() {
|
|
27145
|
-
doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
|
|
27146
|
-
}
|
|
27147
|
-
|
|
27148
27148
|
/*
|
|
27149
27149
|
|
|
27150
27150
|
Logo.ts
|
|
@@ -27254,7 +27254,7 @@ const NetskraflLegend = (initialVnode) => {
|
|
|
27254
27254
|
m.redraw();
|
|
27255
27255
|
}
|
|
27256
27256
|
return {
|
|
27257
|
-
|
|
27257
|
+
oninit: () => {
|
|
27258
27258
|
if (msStepTime && ival === 0) {
|
|
27259
27259
|
ival = setInterval(doStep, msStepTime);
|
|
27260
27260
|
}
|
|
@@ -27309,10 +27309,197 @@ const AnimatedNetskraflLogo = (initialVnode) => {
|
|
|
27309
27309
|
};
|
|
27310
27310
|
};
|
|
27311
27311
|
|
|
27312
|
+
/*
|
|
27313
|
+
|
|
27314
|
+
Login.ts
|
|
27315
|
+
|
|
27316
|
+
Login UI for Metskrafl using the Mithril library
|
|
27317
|
+
|
|
27318
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
27319
|
+
Author: Vilhjálmur Þorsteinsson
|
|
27320
|
+
|
|
27321
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
27322
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
27323
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
27324
|
+
|
|
27325
|
+
This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
|
|
27326
|
+
straightforward JavaScript single-page reactive UI library.
|
|
27327
|
+
|
|
27328
|
+
*/
|
|
27329
|
+
const loginUserByEmail = async (state) => {
|
|
27330
|
+
// Call the /login_malstadur endpoint on the server
|
|
27331
|
+
// to log in the user with the given email and token.
|
|
27332
|
+
// The token is a standard HS256-encoded JWT with aud "netskrafl"
|
|
27333
|
+
// and iss typically "malstadur".
|
|
27334
|
+
const { userEmail, userNick, userFullname, token } = state;
|
|
27335
|
+
return requestWithoutAuth(state, {
|
|
27336
|
+
method: "POST",
|
|
27337
|
+
url: "/login_malstadur",
|
|
27338
|
+
body: { email: userEmail, nickname: userNick, fullname: userFullname, token }
|
|
27339
|
+
});
|
|
27340
|
+
};
|
|
27341
|
+
const LoginError = {
|
|
27342
|
+
view: (vnode) => {
|
|
27343
|
+
return m("div.error", {
|
|
27344
|
+
style: { visibility: "visible" }
|
|
27345
|
+
}, vnode.children);
|
|
27346
|
+
}
|
|
27347
|
+
};
|
|
27348
|
+
const LoginForm = (initialVnode) => {
|
|
27349
|
+
const loginUrl = initialVnode.attrs.loginUrl;
|
|
27350
|
+
let loginInProgress = false;
|
|
27351
|
+
function doLogin(ev) {
|
|
27352
|
+
loginInProgress = true;
|
|
27353
|
+
ev.preventDefault();
|
|
27354
|
+
window.location.href = loginUrl;
|
|
27355
|
+
}
|
|
27356
|
+
return {
|
|
27357
|
+
view: () => {
|
|
27358
|
+
return m.fragment({}, [
|
|
27359
|
+
// This is visible on large screens
|
|
27360
|
+
m("div.loginform-large", [
|
|
27361
|
+
m(NetskraflLogoOnly, {
|
|
27362
|
+
className: "login-logo",
|
|
27363
|
+
width: 200,
|
|
27364
|
+
}),
|
|
27365
|
+
m(NetskraflLegend, {
|
|
27366
|
+
className: "login-legend",
|
|
27367
|
+
width: 600,
|
|
27368
|
+
msStepTime: 0
|
|
27369
|
+
}),
|
|
27370
|
+
mt("div.welcome", "welcome_0"),
|
|
27371
|
+
mt("div.welcome", "welcome_1"),
|
|
27372
|
+
mt("div.welcome", "welcome_2"),
|
|
27373
|
+
m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
|
|
27374
|
+
t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
|
|
27375
|
+
])
|
|
27376
|
+
]),
|
|
27377
|
+
// This is visible on small screens
|
|
27378
|
+
m("div.loginform-small", [
|
|
27379
|
+
m(NetskraflLogoOnly, {
|
|
27380
|
+
className: "login-logo",
|
|
27381
|
+
width: 160,
|
|
27382
|
+
}),
|
|
27383
|
+
m(NetskraflLegend, {
|
|
27384
|
+
className: "login-legend",
|
|
27385
|
+
width: 650,
|
|
27386
|
+
msStepTime: 0
|
|
27387
|
+
}),
|
|
27388
|
+
m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
|
|
27389
|
+
])
|
|
27390
|
+
]);
|
|
27391
|
+
}
|
|
27392
|
+
};
|
|
27393
|
+
};
|
|
27394
|
+
|
|
27395
|
+
// Global state for authentication
|
|
27396
|
+
let authPromise = null;
|
|
27397
|
+
// Custom error class for authentication failures
|
|
27398
|
+
class AuthenticationError extends Error {
|
|
27399
|
+
constructor() {
|
|
27400
|
+
super("Authentication required");
|
|
27401
|
+
this.name = "AuthenticationError";
|
|
27402
|
+
}
|
|
27403
|
+
}
|
|
27404
|
+
// Internal function to ensure authentication
|
|
27405
|
+
const ensureAuthenticated = async (state) => {
|
|
27406
|
+
// If login is already in progress, wait for it to complete
|
|
27407
|
+
if (authPromise) {
|
|
27408
|
+
await authPromise;
|
|
27409
|
+
return;
|
|
27410
|
+
}
|
|
27411
|
+
// Start new login attempt (either forced by 401 or needed for Firebase)
|
|
27412
|
+
authPromise = loginUserByEmail(state);
|
|
27413
|
+
try {
|
|
27414
|
+
const result = await authPromise;
|
|
27415
|
+
if (result.status === "expired") {
|
|
27416
|
+
// Token has expired, notify the React component if callback is set
|
|
27417
|
+
state.tokenExpired && state.tokenExpired();
|
|
27418
|
+
throw new Error("Token expired");
|
|
27419
|
+
}
|
|
27420
|
+
else if (result.status !== "success") {
|
|
27421
|
+
throw new Error(`Authentication failed: ${result.message || result.status}`);
|
|
27422
|
+
}
|
|
27423
|
+
// Success: Log in to Firebase with the token passed from the server
|
|
27424
|
+
await loginFirebase(state, result.firebase_token);
|
|
27425
|
+
}
|
|
27426
|
+
finally {
|
|
27427
|
+
// Reset the promise so future 401s can trigger a new login
|
|
27428
|
+
authPromise = null;
|
|
27429
|
+
}
|
|
27430
|
+
};
|
|
27431
|
+
// Internal authenticated request function
|
|
27432
|
+
const authenticatedRequest = async (state, options, retries = 0) => {
|
|
27433
|
+
// Before making the request, check if Firebase needs authentication
|
|
27434
|
+
// This handles the case where the user returns with a valid backend cookie but expired Firebase auth
|
|
27435
|
+
if (!retries && !isFirebaseAuthenticated(state)) {
|
|
27436
|
+
// Firebase is not authenticated, ensure authentication before making the request
|
|
27437
|
+
await ensureAuthenticated(state);
|
|
27438
|
+
}
|
|
27439
|
+
try {
|
|
27440
|
+
// Make the actual request
|
|
27441
|
+
// console.log("Making authenticated request to", options.url);
|
|
27442
|
+
return await m.request({
|
|
27443
|
+
withCredentials: true,
|
|
27444
|
+
...options,
|
|
27445
|
+
url: serverUrl(state, options.url),
|
|
27446
|
+
extract: (xhr) => {
|
|
27447
|
+
// Check for 401 Unauthorized
|
|
27448
|
+
if (xhr.status === 401) {
|
|
27449
|
+
// console.log("Received 401 Unauthorized, triggering re-authentication");
|
|
27450
|
+
throw new AuthenticationError();
|
|
27451
|
+
}
|
|
27452
|
+
// Handle empty responses
|
|
27453
|
+
if (!xhr.responseText) {
|
|
27454
|
+
return null;
|
|
27455
|
+
}
|
|
27456
|
+
// Parse JSON response
|
|
27457
|
+
try {
|
|
27458
|
+
return JSON.parse(xhr.responseText);
|
|
27459
|
+
}
|
|
27460
|
+
catch (e) {
|
|
27461
|
+
// If JSON parsing fails, return the raw text
|
|
27462
|
+
return xhr.responseText;
|
|
27463
|
+
}
|
|
27464
|
+
}
|
|
27465
|
+
});
|
|
27466
|
+
}
|
|
27467
|
+
catch (error) {
|
|
27468
|
+
if (error instanceof AuthenticationError && !retries) {
|
|
27469
|
+
// Backend returned 401, perform full authentication and retry (force=true)
|
|
27470
|
+
await ensureAuthenticated(state);
|
|
27471
|
+
// Retry the original request
|
|
27472
|
+
return authenticatedRequest(state, options, retries + 1);
|
|
27473
|
+
}
|
|
27474
|
+
// Re-throw other errors
|
|
27475
|
+
throw error;
|
|
27476
|
+
}
|
|
27477
|
+
};
|
|
27478
|
+
const request = (state, options) => {
|
|
27479
|
+
// Enhanced request with automatic authentication handling
|
|
27480
|
+
return authenticatedRequest(state, options);
|
|
27481
|
+
};
|
|
27482
|
+
const requestMoves = (state, options) => {
|
|
27483
|
+
// Call the moves service on the Google App Engine backend
|
|
27484
|
+
const url = `${state.movesUrl}${options.url}`;
|
|
27485
|
+
const headers = {
|
|
27486
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
27487
|
+
Authorization: `Bearer ${state.movesAccessKey}`,
|
|
27488
|
+
...options === null || options === void 0 ? void 0 : options.headers,
|
|
27489
|
+
};
|
|
27490
|
+
return m.request({
|
|
27491
|
+
withCredentials: false,
|
|
27492
|
+
method: "POST",
|
|
27493
|
+
...options,
|
|
27494
|
+
url,
|
|
27495
|
+
headers,
|
|
27496
|
+
});
|
|
27497
|
+
};
|
|
27498
|
+
|
|
27312
27499
|
const SPINNER_INITIAL_DELAY = 800; // milliseconds
|
|
27313
27500
|
const Spinner = {
|
|
27314
27501
|
// Show a spinner wait box, after an initial delay
|
|
27315
|
-
|
|
27502
|
+
oninit: (vnode) => {
|
|
27316
27503
|
vnode.state.show = false;
|
|
27317
27504
|
vnode.state.ival = setTimeout(() => {
|
|
27318
27505
|
vnode.state.show = true;
|
|
@@ -27425,11 +27612,12 @@ const OnlinePresence = (initialVnode) => {
|
|
|
27425
27612
|
const askServer = attrs.online === undefined;
|
|
27426
27613
|
const id = attrs.id;
|
|
27427
27614
|
const userId = attrs.userId;
|
|
27615
|
+
const state = attrs.state;
|
|
27428
27616
|
let loading = false;
|
|
27429
27617
|
async function _update() {
|
|
27430
27618
|
if (askServer && !loading) {
|
|
27431
27619
|
loading = true;
|
|
27432
|
-
const json = await request({
|
|
27620
|
+
const json = await request(state, {
|
|
27433
27621
|
method: "POST",
|
|
27434
27622
|
url: "/onlinecheck",
|
|
27435
27623
|
body: { user: userId }
|
|
@@ -27439,7 +27627,7 @@ const OnlinePresence = (initialVnode) => {
|
|
|
27439
27627
|
}
|
|
27440
27628
|
}
|
|
27441
27629
|
return {
|
|
27442
|
-
|
|
27630
|
+
oninit: _update,
|
|
27443
27631
|
view: (vnode) => {
|
|
27444
27632
|
var _a, _b;
|
|
27445
27633
|
if (!askServer)
|
|
@@ -27705,6 +27893,7 @@ const WaitDialog = (initialVnode) => {
|
|
|
27705
27893
|
const attrs = initialVnode.attrs;
|
|
27706
27894
|
const view = attrs.view;
|
|
27707
27895
|
const model = view.model;
|
|
27896
|
+
const state = model.state;
|
|
27708
27897
|
const duration = attrs.duration;
|
|
27709
27898
|
const oppId = attrs.oppId;
|
|
27710
27899
|
const key = attrs.challengeKey;
|
|
@@ -27720,9 +27909,9 @@ const WaitDialog = (initialVnode) => {
|
|
|
27720
27909
|
async function updateOnline() {
|
|
27721
27910
|
// Initiate an online check on the opponent
|
|
27722
27911
|
try {
|
|
27723
|
-
if (!oppId || !key)
|
|
27912
|
+
if (!oppId || !key || !state)
|
|
27724
27913
|
return;
|
|
27725
|
-
const json = await request({
|
|
27914
|
+
const json = await request(state, {
|
|
27726
27915
|
method: "POST",
|
|
27727
27916
|
url: "/initwait",
|
|
27728
27917
|
body: { opp: oppId, key }
|
|
@@ -27738,8 +27927,10 @@ const WaitDialog = (initialVnode) => {
|
|
|
27738
27927
|
}
|
|
27739
27928
|
async function cancelWait() {
|
|
27740
27929
|
// Cancel a pending wait for a timed game
|
|
27930
|
+
if (!state)
|
|
27931
|
+
return;
|
|
27741
27932
|
try {
|
|
27742
|
-
await request({
|
|
27933
|
+
await request(state, {
|
|
27743
27934
|
method: "POST",
|
|
27744
27935
|
url: "/cancelwait",
|
|
27745
27936
|
body: {
|
|
@@ -27773,11 +27964,13 @@ const WaitDialog = (initialVnode) => {
|
|
|
27773
27964
|
return {
|
|
27774
27965
|
oncreate,
|
|
27775
27966
|
view: () => {
|
|
27967
|
+
if (!state)
|
|
27968
|
+
return null;
|
|
27776
27969
|
return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
|
|
27777
27970
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
27778
27971
|
m("td", m("h1.chall-icon", glyph("time"))),
|
|
27779
27972
|
m("td.l-border", [
|
|
27780
|
-
m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline }),
|
|
27973
|
+
m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
|
|
27781
27974
|
m("h1", oppNick),
|
|
27782
27975
|
m("h2", oppName)
|
|
27783
27976
|
])
|
|
@@ -27820,6 +28013,7 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27820
28013
|
// is linked up with her opponent and a new game is started
|
|
27821
28014
|
const attrs = initialVnode.attrs;
|
|
27822
28015
|
const view = attrs.view;
|
|
28016
|
+
const state = view.model.state;
|
|
27823
28017
|
const oppId = attrs.oppId;
|
|
27824
28018
|
const key = attrs.challengeKey;
|
|
27825
28019
|
let oppNick = attrs.oppNick;
|
|
@@ -27827,11 +28021,11 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27827
28021
|
let loading = false;
|
|
27828
28022
|
async function waitCheck() {
|
|
27829
28023
|
// Initiate a wait status check on the opponent
|
|
27830
|
-
if (loading)
|
|
28024
|
+
if (loading || !state)
|
|
27831
28025
|
return; // Already checking
|
|
27832
28026
|
loading = true;
|
|
27833
28027
|
try {
|
|
27834
|
-
const json = await request({
|
|
28028
|
+
const json = await request(state, {
|
|
27835
28029
|
method: "POST",
|
|
27836
28030
|
url: "/waitcheck",
|
|
27837
28031
|
body: { user: oppId, key }
|
|
@@ -27854,7 +28048,7 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27854
28048
|
}
|
|
27855
28049
|
}
|
|
27856
28050
|
return {
|
|
27857
|
-
|
|
28051
|
+
oninit: waitCheck,
|
|
27858
28052
|
view: () => {
|
|
27859
28053
|
return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
|
|
27860
28054
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
@@ -28039,87 +28233,6 @@ const FriendCancelConfirmDialog = (initialVnode) => {
|
|
|
28039
28233
|
};
|
|
28040
28234
|
};
|
|
28041
28235
|
|
|
28042
|
-
/*
|
|
28043
|
-
|
|
28044
|
-
Login.ts
|
|
28045
|
-
|
|
28046
|
-
Login UI for Metskrafl using the Mithril library
|
|
28047
|
-
|
|
28048
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
28049
|
-
Author: Vilhjálmur Þorsteinsson
|
|
28050
|
-
|
|
28051
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
28052
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
28053
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
28054
|
-
|
|
28055
|
-
This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
|
|
28056
|
-
straightforward JavaScript single-page reactive UI library.
|
|
28057
|
-
|
|
28058
|
-
*/
|
|
28059
|
-
const loginUserByEmail = async (email, nickname, fullname, token) => {
|
|
28060
|
-
// Call the /login_malstadur endpoint on the server
|
|
28061
|
-
// to log in the user with the given email and token.
|
|
28062
|
-
// The token is a standard HS256-encoded JWT with aud "netskrafl"
|
|
28063
|
-
// and iss typically "malstadur".
|
|
28064
|
-
return request({
|
|
28065
|
-
method: "POST",
|
|
28066
|
-
url: "/login_malstadur",
|
|
28067
|
-
body: { email, nickname, fullname, token }
|
|
28068
|
-
});
|
|
28069
|
-
};
|
|
28070
|
-
const LoginError = {
|
|
28071
|
-
view: (vnode) => {
|
|
28072
|
-
var _a;
|
|
28073
|
-
return m("div.error", { style: { visibility: "visible" } }, ((_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.message) || "Error logging in");
|
|
28074
|
-
}
|
|
28075
|
-
};
|
|
28076
|
-
const LoginForm = (initialVnode) => {
|
|
28077
|
-
const loginUrl = initialVnode.attrs.loginUrl;
|
|
28078
|
-
let loginInProgress = false;
|
|
28079
|
-
function doLogin(ev) {
|
|
28080
|
-
loginInProgress = true;
|
|
28081
|
-
ev.preventDefault();
|
|
28082
|
-
window.location.href = loginUrl;
|
|
28083
|
-
}
|
|
28084
|
-
return {
|
|
28085
|
-
view: () => {
|
|
28086
|
-
return m.fragment({}, [
|
|
28087
|
-
// This is visible on large screens
|
|
28088
|
-
m("div.loginform-large", [
|
|
28089
|
-
m(NetskraflLogoOnly, {
|
|
28090
|
-
className: "login-logo",
|
|
28091
|
-
width: 200,
|
|
28092
|
-
}),
|
|
28093
|
-
m(NetskraflLegend, {
|
|
28094
|
-
className: "login-legend",
|
|
28095
|
-
width: 600,
|
|
28096
|
-
msStepTime: 0
|
|
28097
|
-
}),
|
|
28098
|
-
mt("div.welcome", "welcome_0"),
|
|
28099
|
-
mt("div.welcome", "welcome_1"),
|
|
28100
|
-
mt("div.welcome", "welcome_2"),
|
|
28101
|
-
m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
|
|
28102
|
-
t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
|
|
28103
|
-
])
|
|
28104
|
-
]),
|
|
28105
|
-
// This is visible on small screens
|
|
28106
|
-
m("div.loginform-small", [
|
|
28107
|
-
m(NetskraflLogoOnly, {
|
|
28108
|
-
className: "login-logo",
|
|
28109
|
-
width: 160,
|
|
28110
|
-
}),
|
|
28111
|
-
m(NetskraflLegend, {
|
|
28112
|
-
className: "login-legend",
|
|
28113
|
-
width: 650,
|
|
28114
|
-
msStepTime: 0
|
|
28115
|
-
}),
|
|
28116
|
-
m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
|
|
28117
|
-
])
|
|
28118
|
-
]);
|
|
28119
|
-
}
|
|
28120
|
-
};
|
|
28121
|
-
};
|
|
28122
|
-
|
|
28123
28236
|
/*
|
|
28124
28237
|
|
|
28125
28238
|
ChallengeDialog.ts
|
|
@@ -28150,7 +28263,11 @@ const ChallengeDialog = () => {
|
|
|
28150
28263
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
28151
28264
|
m("td", m("h1.chall-icon", glyph("hand-right"))),
|
|
28152
28265
|
m("td.l-border", [
|
|
28153
|
-
m(OnlinePresence, {
|
|
28266
|
+
m(OnlinePresence, {
|
|
28267
|
+
id: "chall-online",
|
|
28268
|
+
userId: item.userid,
|
|
28269
|
+
state,
|
|
28270
|
+
}),
|
|
28154
28271
|
m("h1", item.nick),
|
|
28155
28272
|
m("h2", item.fullname)
|
|
28156
28273
|
])
|
|
@@ -29466,7 +29583,7 @@ const PromoDialog = (initialVnode) => {
|
|
|
29466
29583
|
initFunc();
|
|
29467
29584
|
}
|
|
29468
29585
|
return {
|
|
29469
|
-
|
|
29586
|
+
oninit: _fetchContent,
|
|
29470
29587
|
view: (vnode) => {
|
|
29471
29588
|
let initFunc = vnode.attrs.initFunc;
|
|
29472
29589
|
return m(".modal-dialog", { id: "promo-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "promo-form", className: "promo-" + vnode.attrs.kind }, m("div", {
|
|
@@ -29538,7 +29655,7 @@ const UserInfoDialog = (initialVnode) => {
|
|
|
29538
29655
|
}
|
|
29539
29656
|
}
|
|
29540
29657
|
return {
|
|
29541
|
-
|
|
29658
|
+
oninit: (vnode) => {
|
|
29542
29659
|
_updateRecentList(vnode);
|
|
29543
29660
|
_updateStats(vnode);
|
|
29544
29661
|
},
|
|
@@ -30528,7 +30645,9 @@ const Board = (initialVnode) => {
|
|
|
30528
30645
|
const scale = view.boardScale || 1.0;
|
|
30529
30646
|
let attrs = {};
|
|
30530
30647
|
// Add handlers for pinch zoom functionality
|
|
30531
|
-
|
|
30648
|
+
// Note: resist the temptation to pass zoomIn/zoomOut directly,
|
|
30649
|
+
// as that would not bind the 'this' pointer correctly
|
|
30650
|
+
addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
|
|
30532
30651
|
if (scale !== 1.0)
|
|
30533
30652
|
attrs.style = `transform: scale(${scale})`;
|
|
30534
30653
|
return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
|
|
@@ -30706,7 +30825,7 @@ const Chat = (initialVnode) => {
|
|
|
30706
30825
|
const { view } = initialVnode.attrs;
|
|
30707
30826
|
const model = view.model;
|
|
30708
30827
|
const game = model.game;
|
|
30709
|
-
model.state;
|
|
30828
|
+
const state = model.state;
|
|
30710
30829
|
function decodeTimestamp(ts) {
|
|
30711
30830
|
// Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
|
|
30712
30831
|
return {
|
|
@@ -30763,7 +30882,7 @@ const Chat = (initialVnode) => {
|
|
|
30763
30882
|
for (const emoticon of EMOTICONS)
|
|
30764
30883
|
if (str.indexOf(emoticon.icon) >= 0) {
|
|
30765
30884
|
// The string contains the emoticon: prepare to replace all occurrences
|
|
30766
|
-
const imgUrl = serverUrl(emoticon.image);
|
|
30885
|
+
const imgUrl = serverUrl(state, emoticon.image);
|
|
30767
30886
|
const img = `<img src='${imgUrl}' height='32' width='32'>`;
|
|
30768
30887
|
// Re the following trick, see https://stackoverflow.com/questions/1144783/
|
|
30769
30888
|
// replacing-all-occurrences-of-a-string-in-javascript
|
|
@@ -31914,13 +32033,13 @@ class View {
|
|
|
31914
32033
|
views.push(this.vwLogin());
|
|
31915
32034
|
break;
|
|
31916
32035
|
case "loginerror":
|
|
31917
|
-
views.push(m(LoginError));
|
|
32036
|
+
views.push(m(LoginError, { key: "login-error" }, t("Villa við innskráningu")));
|
|
31918
32037
|
break;
|
|
31919
32038
|
case "main":
|
|
31920
|
-
views.push(m(Main, { view: this }));
|
|
32039
|
+
views.push(m(Main, { key: "main", view: this }));
|
|
31921
32040
|
break;
|
|
31922
32041
|
case "game":
|
|
31923
|
-
views.push(m(GameView, { view: this }));
|
|
32042
|
+
views.push(m(GameView, { key: "game", view: this }));
|
|
31924
32043
|
break;
|
|
31925
32044
|
case "review":
|
|
31926
32045
|
const n = vwReview(this);
|
|
@@ -31928,7 +32047,7 @@ class View {
|
|
|
31928
32047
|
break;
|
|
31929
32048
|
case "thanks":
|
|
31930
32049
|
// Display a thank-you dialog on top of the normal main screen
|
|
31931
|
-
views.push(m(Main, { view: this }));
|
|
32050
|
+
views.push(m(Main, { key: "main", view: this }));
|
|
31932
32051
|
// Be careful to add the Thanks dialog only once to the stack
|
|
31933
32052
|
if (!this.dialogStack.length)
|
|
31934
32053
|
this.showThanks();
|
|
@@ -31939,7 +32058,7 @@ class View {
|
|
|
31939
32058
|
views.push(this.vwHelp(parseInt(m.route.param("tab") || ""), parseInt(m.route.param("faq") || "")));
|
|
31940
32059
|
break;
|
|
31941
32060
|
default:
|
|
31942
|
-
return [m("div", t("Þessi vefslóð er ekki rétt"))];
|
|
32061
|
+
return [m("div", { key: "error" }, t("Þessi vefslóð er ekki rétt"))];
|
|
31943
32062
|
}
|
|
31944
32063
|
// Push any open dialogs
|
|
31945
32064
|
for (const dialog of this.dialogStack) {
|
|
@@ -31954,7 +32073,7 @@ class View {
|
|
|
31954
32073
|
}
|
|
31955
32074
|
// Overlay a spinner, if active
|
|
31956
32075
|
if (model.spinners)
|
|
31957
|
-
views.push(m(Spinner));
|
|
32076
|
+
views.push(m(Spinner, { key: "spinner" }));
|
|
31958
32077
|
return views;
|
|
31959
32078
|
}
|
|
31960
32079
|
// Dialog support
|
|
@@ -32035,7 +32154,7 @@ class View {
|
|
|
32035
32154
|
zoomOut() {
|
|
32036
32155
|
if (this.boardScale !== 1.0) {
|
|
32037
32156
|
this.boardScale = 1.0;
|
|
32038
|
-
setTimeout(this.resetScale);
|
|
32157
|
+
setTimeout(() => this.resetScale());
|
|
32039
32158
|
}
|
|
32040
32159
|
}
|
|
32041
32160
|
resetScale() {
|
|
@@ -32082,7 +32201,7 @@ class View {
|
|
|
32082
32201
|
// No game or we're in full screen mode: always 100% scale
|
|
32083
32202
|
// Also, as soon as a move is being processed by the server, we zoom out
|
|
32084
32203
|
this.boardScale = 1.0; // Needs to be done before setTimeout() call
|
|
32085
|
-
setTimeout(this.resetScale);
|
|
32204
|
+
setTimeout(() => this.resetScale());
|
|
32086
32205
|
return;
|
|
32087
32206
|
}
|
|
32088
32207
|
const tp = game.tilesPlaced();
|
|
@@ -32176,7 +32295,7 @@ class View {
|
|
|
32176
32295
|
}
|
|
32177
32296
|
}
|
|
32178
32297
|
// Output literal HTML obtained from rawhelp.html on the server
|
|
32179
|
-
return m.fragment({}, [
|
|
32298
|
+
return m.fragment({ key: "help" }, [
|
|
32180
32299
|
m(LeftLogo),
|
|
32181
32300
|
m(UserId, { view: this }),
|
|
32182
32301
|
this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
|
|
@@ -32222,6 +32341,7 @@ class View {
|
|
|
32222
32341
|
vnode.dom.querySelector("#nickname").focus();
|
|
32223
32342
|
}
|
|
32224
32343
|
return m(".modal-dialog", {
|
|
32344
|
+
key: "userprefs",
|
|
32225
32345
|
id: "user-dialog",
|
|
32226
32346
|
oncreate: initFocus
|
|
32227
32347
|
// onupdate: initFocus
|
|
@@ -32303,11 +32423,12 @@ class View {
|
|
|
32303
32423
|
model.loadUser(true); // Activate spinner while loading
|
|
32304
32424
|
if (!model.user)
|
|
32305
32425
|
// Nothing to edit (the spinner should be showing in this case)
|
|
32306
|
-
return m.fragment({}, []);
|
|
32426
|
+
return m.fragment({ key: "userprefs-empty" }, []);
|
|
32307
32427
|
return this.vwUserPrefsDialog();
|
|
32308
32428
|
}
|
|
32309
32429
|
vwUserInfo(args) {
|
|
32310
32430
|
return m(UserInfoDialog, {
|
|
32431
|
+
key: "userinfodialog-" + args.userid,
|
|
32311
32432
|
view: this,
|
|
32312
32433
|
userid: args.userid,
|
|
32313
32434
|
nick: args.nick,
|
|
@@ -32316,6 +32437,7 @@ class View {
|
|
|
32316
32437
|
}
|
|
32317
32438
|
vwPromo(args) {
|
|
32318
32439
|
return m(PromoDialog, {
|
|
32440
|
+
key: "promo-" + args.kind,
|
|
32319
32441
|
view: this,
|
|
32320
32442
|
kind: args.kind,
|
|
32321
32443
|
initFunc: args.initFunc
|
|
@@ -32323,6 +32445,7 @@ class View {
|
|
|
32323
32445
|
}
|
|
32324
32446
|
vwWait(args) {
|
|
32325
32447
|
return m(WaitDialog, {
|
|
32448
|
+
key: "wait-" + args.challengeKey,
|
|
32326
32449
|
view: this,
|
|
32327
32450
|
oppId: args.oppId,
|
|
32328
32451
|
oppNick: args.oppNick,
|
|
@@ -32333,6 +32456,7 @@ class View {
|
|
|
32333
32456
|
}
|
|
32334
32457
|
vwAccept(args) {
|
|
32335
32458
|
return m(AcceptDialog, {
|
|
32459
|
+
key: "accept-" + args.challengeKey,
|
|
32336
32460
|
view: this,
|
|
32337
32461
|
oppId: args.oppId,
|
|
32338
32462
|
oppNick: args.oppNick,
|
|
@@ -32343,7 +32467,7 @@ class View {
|
|
|
32343
32467
|
var _a;
|
|
32344
32468
|
const model = this.model;
|
|
32345
32469
|
const loginUrl = ((_a = model.state) === null || _a === void 0 ? void 0 : _a.loginUrl) || "";
|
|
32346
|
-
return m(LoginForm, { loginUrl });
|
|
32470
|
+
return m(LoginForm, { key: "login", loginUrl });
|
|
32347
32471
|
}
|
|
32348
32472
|
vwDialogs() {
|
|
32349
32473
|
// Show prompt dialogs below game board, if any
|
|
@@ -32422,12 +32546,12 @@ class View {
|
|
|
32422
32546
|
View.dialogViews = {
|
|
32423
32547
|
userprefs: (view) => view.vwUserPrefs(),
|
|
32424
32548
|
userinfo: (view, args) => view.vwUserInfo(args),
|
|
32425
|
-
challenge: (view, args) => m(ChallengeDialog, { view, item: args }),
|
|
32549
|
+
challenge: (view, args) => m(ChallengeDialog, { key: "challenge-" + args.item.challenge_key, view, item: args }),
|
|
32426
32550
|
promo: (view, args) => view.vwPromo(args),
|
|
32427
|
-
friend: (view) => m(FriendPromoteDialog, { view }),
|
|
32428
|
-
thanks: (view) => m(FriendThanksDialog, { view }),
|
|
32429
|
-
cancel: (view) => m(FriendCancelDialog, { view }),
|
|
32430
|
-
confirm: (view) => m(FriendCancelConfirmDialog, { view }),
|
|
32551
|
+
friend: (view) => m(FriendPromoteDialog, { key: "friend", view }),
|
|
32552
|
+
thanks: (view) => m(FriendThanksDialog, { key: "thanks", view }),
|
|
32553
|
+
cancel: (view) => m(FriendCancelDialog, { key: "cancel", view }),
|
|
32554
|
+
confirm: (view) => m(FriendCancelConfirmDialog, { key: "confirm", view }),
|
|
32431
32555
|
wait: (view, args) => view.vwWait(args),
|
|
32432
32556
|
accept: (view, args) => view.vwAccept(args)
|
|
32433
32557
|
};
|
|
@@ -32465,7 +32589,7 @@ class WordChecker {
|
|
|
32465
32589
|
}
|
|
32466
32590
|
}
|
|
32467
32591
|
}
|
|
32468
|
-
async checkWords(locale, words) {
|
|
32592
|
+
async checkWords(state, locale, words) {
|
|
32469
32593
|
// Return true if all words are valid in the given locale,
|
|
32470
32594
|
// or false otherwise. Lookups are cached for efficiency.
|
|
32471
32595
|
let cache = this.wordCheckCache[locale];
|
|
@@ -32493,7 +32617,7 @@ class WordChecker {
|
|
|
32493
32617
|
}
|
|
32494
32618
|
// We need a server roundtrip
|
|
32495
32619
|
try {
|
|
32496
|
-
const response = await requestMoves({
|
|
32620
|
+
const response = await requestMoves(state, {
|
|
32497
32621
|
url: "/wordcheck",
|
|
32498
32622
|
body: {
|
|
32499
32623
|
locale,
|
|
@@ -32723,7 +32847,7 @@ const BOARD = {
|
|
|
32723
32847
|
}
|
|
32724
32848
|
};
|
|
32725
32849
|
class BaseGame {
|
|
32726
|
-
constructor(uuid, board_type = "standard") {
|
|
32850
|
+
constructor(uuid, state, board_type = "standard") {
|
|
32727
32851
|
// Basic game properties that don't change while the game is underway
|
|
32728
32852
|
this.locale = "is_IS";
|
|
32729
32853
|
this.alphabet = "";
|
|
@@ -32757,6 +32881,7 @@ class BaseGame {
|
|
|
32757
32881
|
this.board_type = board_type;
|
|
32758
32882
|
this.startSquare = START_SQUARE[board_type];
|
|
32759
32883
|
this.startCoord = START_COORD[board_type];
|
|
32884
|
+
this.state = state;
|
|
32760
32885
|
}
|
|
32761
32886
|
// Default init method that can be overridden
|
|
32762
32887
|
init() {
|
|
@@ -32997,7 +33122,7 @@ class BaseGame {
|
|
|
32997
33122
|
if (!this.manual) {
|
|
32998
33123
|
// This is not a manual-wordcheck game:
|
|
32999
33124
|
// Check the word that has been laid down
|
|
33000
|
-
const found = await wordChecker.checkWords(this.locale, scoreResult.words);
|
|
33125
|
+
const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
|
|
33001
33126
|
this.wordGood = found;
|
|
33002
33127
|
this.wordBad = !found;
|
|
33003
33128
|
}
|
|
@@ -33260,10 +33385,10 @@ const MAX_OVERTIME = 10 * 60.0;
|
|
|
33260
33385
|
const DEBUG_OVERTIME = 1 * 60.0;
|
|
33261
33386
|
const GAME_OVER = 99; // Error code corresponding to the Error class in skraflmechanics.py
|
|
33262
33387
|
class Game extends BaseGame {
|
|
33263
|
-
constructor(uuid, srvGame, moveListener, maxOvertime) {
|
|
33388
|
+
constructor(uuid, srvGame, moveListener, state, maxOvertime) {
|
|
33264
33389
|
var _a;
|
|
33265
33390
|
// Call parent constructor
|
|
33266
|
-
super(uuid); // Default board_type: "standard"
|
|
33391
|
+
super(uuid, state); // Default board_type: "standard"
|
|
33267
33392
|
// A class that represents a Netskrafl game instance on the client
|
|
33268
33393
|
// Netskrafl-specific properties
|
|
33269
33394
|
this.userid = ["", ""];
|
|
@@ -33520,7 +33645,7 @@ class Game extends BaseGame {
|
|
|
33520
33645
|
try {
|
|
33521
33646
|
if (!this.uuid)
|
|
33522
33647
|
return;
|
|
33523
|
-
const result = await request({
|
|
33648
|
+
const result = await request(this.state, {
|
|
33524
33649
|
method: "POST",
|
|
33525
33650
|
url: "/gamestate",
|
|
33526
33651
|
body: { game: this.uuid } // !!! FIXME: Add delete_zombie parameter
|
|
@@ -33723,7 +33848,7 @@ class Game extends BaseGame {
|
|
|
33723
33848
|
this.chatLoading = true;
|
|
33724
33849
|
this.messages = [];
|
|
33725
33850
|
try {
|
|
33726
|
-
const result = await request({
|
|
33851
|
+
const result = await request(this.state, {
|
|
33727
33852
|
method: "POST",
|
|
33728
33853
|
url: "/chatload",
|
|
33729
33854
|
body: { channel: "game:" + this.uuid }
|
|
@@ -33749,7 +33874,7 @@ class Game extends BaseGame {
|
|
|
33749
33874
|
// Load statistics about a game
|
|
33750
33875
|
this.stats = undefined; // Error/in-progress status
|
|
33751
33876
|
try {
|
|
33752
|
-
const json = await request({
|
|
33877
|
+
const json = await request(this.state, {
|
|
33753
33878
|
method: "POST",
|
|
33754
33879
|
url: "/gamestats",
|
|
33755
33880
|
body: { game: this.uuid }
|
|
@@ -33769,7 +33894,7 @@ class Game extends BaseGame {
|
|
|
33769
33894
|
async sendMessage(msg) {
|
|
33770
33895
|
// Send a chat message
|
|
33771
33896
|
try {
|
|
33772
|
-
await request({
|
|
33897
|
+
await request(this.state, {
|
|
33773
33898
|
method: "POST",
|
|
33774
33899
|
url: "/chatmsg",
|
|
33775
33900
|
body: { channel: "game:" + this.uuid, msg: msg }
|
|
@@ -33971,10 +34096,10 @@ class Game extends BaseGame {
|
|
|
33971
34096
|
// Send a move to the server
|
|
33972
34097
|
this.moveInProgress = true;
|
|
33973
34098
|
try {
|
|
33974
|
-
const result = await request({
|
|
34099
|
+
const result = await request(this.state, {
|
|
33975
34100
|
method: "POST",
|
|
33976
34101
|
url: "/submitmove",
|
|
33977
|
-
body: { moves: moves, mcount: this.moves.length, uuid: this.uuid }
|
|
34102
|
+
body: { moves: moves, mcount: this.moves.length, uuid: this.uuid },
|
|
33978
34103
|
});
|
|
33979
34104
|
// The update() function also handles error results
|
|
33980
34105
|
this.update(result);
|
|
@@ -34000,7 +34125,7 @@ class Game extends BaseGame {
|
|
|
34000
34125
|
// Force resignation by a tardy opponent
|
|
34001
34126
|
this.moveInProgress = true;
|
|
34002
34127
|
try {
|
|
34003
|
-
const result = await request({
|
|
34128
|
+
const result = await request(this.state, {
|
|
34004
34129
|
method: "POST",
|
|
34005
34130
|
url: "/forceresign",
|
|
34006
34131
|
body: { mcount: this.moves.length, game: this.uuid }
|
|
@@ -34124,7 +34249,10 @@ const HOT_WARM_BOUNDARY_RATIO = 0.6;
|
|
|
34124
34249
|
const WARM_COLD_BOUNDARY_RATIO = 0.3;
|
|
34125
34250
|
class Riddle extends BaseGame {
|
|
34126
34251
|
constructor(uuid, date, model) {
|
|
34127
|
-
|
|
34252
|
+
if (!model.state) {
|
|
34253
|
+
throw new Error("No global state in Riddle constructor");
|
|
34254
|
+
}
|
|
34255
|
+
super(uuid, model.state);
|
|
34128
34256
|
// Scoring properties, static
|
|
34129
34257
|
this.bestPossibleScore = 0;
|
|
34130
34258
|
this.warmBoundary = 0;
|
|
@@ -34165,9 +34293,12 @@ class Riddle extends BaseGame {
|
|
|
34165
34293
|
async load(date, locale) {
|
|
34166
34294
|
this.date = date;
|
|
34167
34295
|
this.locale = locale;
|
|
34296
|
+
const { state } = this;
|
|
34168
34297
|
try {
|
|
34298
|
+
if (!state)
|
|
34299
|
+
throw new Error("No global state in Riddle.load");
|
|
34169
34300
|
// Request riddle data from server (HTTP API call)
|
|
34170
|
-
const response = await request({
|
|
34301
|
+
const response = await request(state, {
|
|
34171
34302
|
method: "POST",
|
|
34172
34303
|
url: "/gatadagsins/riddle",
|
|
34173
34304
|
body: { date, locale }
|
|
@@ -34195,11 +34326,11 @@ class Riddle extends BaseGame {
|
|
|
34195
34326
|
}
|
|
34196
34327
|
}
|
|
34197
34328
|
async submitRiddleWord(move) {
|
|
34198
|
-
const { state } = this
|
|
34329
|
+
const { state } = this;
|
|
34199
34330
|
if (!state || !state.userId)
|
|
34200
34331
|
return;
|
|
34201
34332
|
try {
|
|
34202
|
-
await request({
|
|
34333
|
+
await request(state, {
|
|
34203
34334
|
method: "POST",
|
|
34204
34335
|
url: "/gatadagsins/submit",
|
|
34205
34336
|
body: {
|
|
@@ -34479,8 +34610,6 @@ function getSettings() {
|
|
|
34479
34610
|
}
|
|
34480
34611
|
class Model {
|
|
34481
34612
|
constructor(settings, state) {
|
|
34482
|
-
// A class for the underlying data model, displayed by the current view
|
|
34483
|
-
this.state = null;
|
|
34484
34613
|
this.paths = [];
|
|
34485
34614
|
// The routeName will be "login", "main", "game"...
|
|
34486
34615
|
this.routeName = undefined;
|
|
@@ -34539,7 +34668,54 @@ class Model {
|
|
|
34539
34668
|
this.isExplo = state.isExplo;
|
|
34540
34669
|
this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
|
|
34541
34670
|
// Load localized text messages from the messages.json file
|
|
34542
|
-
loadMessages(state.locale);
|
|
34671
|
+
loadMessages(state, state.locale);
|
|
34672
|
+
}
|
|
34673
|
+
// Simple POST request with JSON body (most common case)
|
|
34674
|
+
async post(url, body) {
|
|
34675
|
+
if (!this.state) {
|
|
34676
|
+
throw new Error("Model state is not initialized");
|
|
34677
|
+
}
|
|
34678
|
+
return request(this.state, {
|
|
34679
|
+
method: "POST",
|
|
34680
|
+
url,
|
|
34681
|
+
body
|
|
34682
|
+
});
|
|
34683
|
+
}
|
|
34684
|
+
// GET request for HTML/text content
|
|
34685
|
+
async getText(url) {
|
|
34686
|
+
if (!this.state) {
|
|
34687
|
+
throw new Error("Model state is not initialized");
|
|
34688
|
+
}
|
|
34689
|
+
return request(this.state, {
|
|
34690
|
+
method: "GET",
|
|
34691
|
+
url,
|
|
34692
|
+
responseType: "text",
|
|
34693
|
+
deserialize: (str) => str
|
|
34694
|
+
});
|
|
34695
|
+
}
|
|
34696
|
+
// POST request that returns text/HTML
|
|
34697
|
+
async postText(url, body) {
|
|
34698
|
+
if (!this.state) {
|
|
34699
|
+
throw new Error("Model state is not initialized");
|
|
34700
|
+
}
|
|
34701
|
+
return request(this.state, {
|
|
34702
|
+
method: "POST",
|
|
34703
|
+
url,
|
|
34704
|
+
body,
|
|
34705
|
+
responseType: "text",
|
|
34706
|
+
deserialize: (str) => str
|
|
34707
|
+
});
|
|
34708
|
+
}
|
|
34709
|
+
// Request to the moves service
|
|
34710
|
+
async postMoves(body) {
|
|
34711
|
+
if (!this.state) {
|
|
34712
|
+
throw new Error("Model state is not initialized");
|
|
34713
|
+
}
|
|
34714
|
+
return requestMoves(this.state, {
|
|
34715
|
+
method: "POST",
|
|
34716
|
+
url: "/moves",
|
|
34717
|
+
body
|
|
34718
|
+
});
|
|
34543
34719
|
}
|
|
34544
34720
|
async loadGame(uuid, funcComplete, deleteZombie = false) {
|
|
34545
34721
|
var _a;
|
|
@@ -34558,20 +34734,16 @@ class Model {
|
|
|
34558
34734
|
this.highlightedMove = null;
|
|
34559
34735
|
if (!uuid)
|
|
34560
34736
|
return; // Should not happen
|
|
34561
|
-
const result = await
|
|
34562
|
-
|
|
34563
|
-
|
|
34564
|
-
body: {
|
|
34565
|
-
game: uuid,
|
|
34566
|
-
delete_zombie: deleteZombie
|
|
34567
|
-
}
|
|
34737
|
+
const result = await this.post("/gamestate", {
|
|
34738
|
+
game: uuid,
|
|
34739
|
+
delete_zombie: deleteZombie
|
|
34568
34740
|
});
|
|
34569
34741
|
if (!(result === null || result === void 0 ? void 0 : result.ok)) {
|
|
34570
34742
|
// console.log("Game " + uuid + " could not be loaded");
|
|
34571
34743
|
}
|
|
34572
34744
|
else {
|
|
34573
34745
|
// Create a new game instance and load the state into it
|
|
34574
|
-
this.game = new Game(uuid, result.game, this, ((_a = this.state) === null || _a === void 0 ? void 0 : _a.runningLocal) ? DEBUG_OVERTIME : MAX_OVERTIME);
|
|
34746
|
+
this.game = new Game(uuid, result.game, this, this.state, ((_a = this.state) === null || _a === void 0 ? void 0 : _a.runningLocal) ? DEBUG_OVERTIME : MAX_OVERTIME);
|
|
34575
34747
|
// Successfully loaded: call the completion function, if given
|
|
34576
34748
|
// (this usually attaches the Firebase event listener)
|
|
34577
34749
|
funcComplete && funcComplete();
|
|
@@ -34594,11 +34766,7 @@ class Model {
|
|
|
34594
34766
|
this.numChallenges = 0;
|
|
34595
34767
|
this.oppReady = 0;
|
|
34596
34768
|
try {
|
|
34597
|
-
const json = await
|
|
34598
|
-
method: "POST",
|
|
34599
|
-
url: "/allgamelists",
|
|
34600
|
-
body: { zombie: includeZombies, count: 40 }
|
|
34601
|
-
});
|
|
34769
|
+
const json = await this.post("/allgamelists", { zombie: includeZombies, count: 40 });
|
|
34602
34770
|
if (!json || json.result !== 0) {
|
|
34603
34771
|
// An error occurred
|
|
34604
34772
|
this.gameList = [];
|
|
@@ -34640,11 +34808,7 @@ class Model {
|
|
|
34640
34808
|
this.numGames = 0;
|
|
34641
34809
|
this.spinners++;
|
|
34642
34810
|
try {
|
|
34643
|
-
const json = await
|
|
34644
|
-
method: "POST",
|
|
34645
|
-
url: "/gamelist",
|
|
34646
|
-
body: { zombie: includeZombies }
|
|
34647
|
-
});
|
|
34811
|
+
const json = await this.post("/gamelist", { zombie: includeZombies });
|
|
34648
34812
|
if (!json || json.result !== 0) {
|
|
34649
34813
|
// An error occurred
|
|
34650
34814
|
this.gameList = [];
|
|
@@ -34674,10 +34838,7 @@ class Model {
|
|
|
34674
34838
|
this.oppReady = 0;
|
|
34675
34839
|
this.spinners++; // Show spinner while loading
|
|
34676
34840
|
try {
|
|
34677
|
-
const json = await
|
|
34678
|
-
method: "POST",
|
|
34679
|
-
url: "/challengelist"
|
|
34680
|
-
});
|
|
34841
|
+
const json = await this.post("/challengelist");
|
|
34681
34842
|
if (!json || json.result !== 0) {
|
|
34682
34843
|
// An error occurred
|
|
34683
34844
|
this.challengeList = [];
|
|
@@ -34711,11 +34872,7 @@ class Model {
|
|
|
34711
34872
|
this.recentList = [];
|
|
34712
34873
|
this.spinners++; // Show spinner while loading
|
|
34713
34874
|
try {
|
|
34714
|
-
const json = await
|
|
34715
|
-
method: "POST",
|
|
34716
|
-
url: "/recentlist",
|
|
34717
|
-
body: { versus: null, count: 40 }
|
|
34718
|
-
});
|
|
34875
|
+
const json = await this.post("/recentlist", { versus: null, count: 40 });
|
|
34719
34876
|
if (!json || json.result !== 0) {
|
|
34720
34877
|
// An error occurred
|
|
34721
34878
|
this.recentList = [];
|
|
@@ -34734,11 +34891,7 @@ class Model {
|
|
|
34734
34891
|
}
|
|
34735
34892
|
async loadUserRecentList(userid, versus, readyFunc) {
|
|
34736
34893
|
// Load the list of recent games for the given user
|
|
34737
|
-
const json = await
|
|
34738
|
-
method: "POST",
|
|
34739
|
-
url: "/recentlist",
|
|
34740
|
-
body: { user: userid, versus: versus, count: 40 }
|
|
34741
|
-
});
|
|
34894
|
+
const json = await this.post("/recentlist", { user: userid, versus: versus, count: 40 });
|
|
34742
34895
|
readyFunc(json);
|
|
34743
34896
|
}
|
|
34744
34897
|
async loadUserList(criteria) {
|
|
@@ -34756,11 +34909,7 @@ class Model {
|
|
|
34756
34909
|
const url = "/userlist";
|
|
34757
34910
|
const body = criteria;
|
|
34758
34911
|
try {
|
|
34759
|
-
const json = await
|
|
34760
|
-
method: "POST",
|
|
34761
|
-
url,
|
|
34762
|
-
body,
|
|
34763
|
-
});
|
|
34912
|
+
const json = await this.post(url, body);
|
|
34764
34913
|
if (!json || json.result !== 0) {
|
|
34765
34914
|
// An error occurred
|
|
34766
34915
|
this.userList = [];
|
|
@@ -34782,11 +34931,7 @@ class Model {
|
|
|
34782
34931
|
const url = "/rating";
|
|
34783
34932
|
const body = { kind: spec };
|
|
34784
34933
|
try {
|
|
34785
|
-
const json = await
|
|
34786
|
-
method: "POST",
|
|
34787
|
-
url,
|
|
34788
|
-
body,
|
|
34789
|
-
});
|
|
34934
|
+
const json = await this.post(url, body);
|
|
34790
34935
|
if (!json || json.result !== 0) {
|
|
34791
34936
|
// An error occurred
|
|
34792
34937
|
this.eloRatingList = [];
|
|
@@ -34805,11 +34950,7 @@ class Model {
|
|
|
34805
34950
|
// Load statistics for the current user
|
|
34806
34951
|
this.ownStats = {};
|
|
34807
34952
|
try {
|
|
34808
|
-
const json = await
|
|
34809
|
-
method: "POST",
|
|
34810
|
-
url: "/userstats",
|
|
34811
|
-
body: {} // Current user is implicit
|
|
34812
|
-
});
|
|
34953
|
+
const json = await this.post("/userstats", {});
|
|
34813
34954
|
if (!json || json.result !== 0) {
|
|
34814
34955
|
// An error occurred
|
|
34815
34956
|
return;
|
|
@@ -34822,11 +34963,7 @@ class Model {
|
|
|
34822
34963
|
async loadUserStats(userid, readyFunc) {
|
|
34823
34964
|
// Load statistics for the given user
|
|
34824
34965
|
try {
|
|
34825
|
-
const json = await
|
|
34826
|
-
method: "POST",
|
|
34827
|
-
url: "/userstats",
|
|
34828
|
-
body: { user: userid }
|
|
34829
|
-
});
|
|
34966
|
+
const json = await this.post("/userstats", { user: userid });
|
|
34830
34967
|
readyFunc(json);
|
|
34831
34968
|
}
|
|
34832
34969
|
catch (e) {
|
|
@@ -34836,13 +34973,7 @@ class Model {
|
|
|
34836
34973
|
async loadPromoContent(key, readyFunc) {
|
|
34837
34974
|
// Load HTML content for promo dialog
|
|
34838
34975
|
try {
|
|
34839
|
-
const html = await
|
|
34840
|
-
method: "POST",
|
|
34841
|
-
url: "/promo",
|
|
34842
|
-
body: { key: key },
|
|
34843
|
-
responseType: "text",
|
|
34844
|
-
deserialize: (str) => str
|
|
34845
|
-
});
|
|
34976
|
+
const html = await this.postText("/promo", { key: key });
|
|
34846
34977
|
readyFunc(html);
|
|
34847
34978
|
}
|
|
34848
34979
|
catch (e) {
|
|
@@ -34891,11 +35022,7 @@ class Model {
|
|
|
34891
35022
|
rack,
|
|
34892
35023
|
limit: NUM_BEST_MOVES,
|
|
34893
35024
|
};
|
|
34894
|
-
const json = await
|
|
34895
|
-
method: "POST",
|
|
34896
|
-
url: "/moves",
|
|
34897
|
-
body: rq,
|
|
34898
|
-
});
|
|
35025
|
+
const json = await this.postMoves(rq);
|
|
34899
35026
|
this.highlightedMove = null;
|
|
34900
35027
|
if (!json || json.moves === undefined) {
|
|
34901
35028
|
// Something unexpected going on
|
|
@@ -34930,12 +35057,7 @@ class Model {
|
|
|
34930
35057
|
return; // Already loaded
|
|
34931
35058
|
try {
|
|
34932
35059
|
const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
34933
|
-
const result = await
|
|
34934
|
-
method: "GET",
|
|
34935
|
-
url: "/rawhelp?version=malstadur&locale=" + locale,
|
|
34936
|
-
responseType: "text",
|
|
34937
|
-
deserialize: (str) => str
|
|
34938
|
-
});
|
|
35060
|
+
const result = await this.getText("/rawhelp?version=malstadur&locale=" + locale);
|
|
34939
35061
|
this.helpHTML = result;
|
|
34940
35062
|
}
|
|
34941
35063
|
catch (e) {
|
|
@@ -34950,12 +35072,7 @@ class Model {
|
|
|
34950
35072
|
return; // Already loaded
|
|
34951
35073
|
try {
|
|
34952
35074
|
const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
34953
|
-
const result = await
|
|
34954
|
-
method: "GET",
|
|
34955
|
-
url: "/friend?locale=" + locale,
|
|
34956
|
-
responseType: "text",
|
|
34957
|
-
deserialize: (str) => str
|
|
34958
|
-
});
|
|
35075
|
+
const result = await this.getText("/friend?locale=" + locale);
|
|
34959
35076
|
this.friendHTML = result;
|
|
34960
35077
|
}
|
|
34961
35078
|
catch (e) {
|
|
@@ -34971,10 +35088,7 @@ class Model {
|
|
|
34971
35088
|
this.spinners++;
|
|
34972
35089
|
}
|
|
34973
35090
|
try {
|
|
34974
|
-
const result = await
|
|
34975
|
-
method: "POST",
|
|
34976
|
-
url: "/loaduserprefs",
|
|
34977
|
-
});
|
|
35091
|
+
const result = await this.post("/loaduserprefs");
|
|
34978
35092
|
if (!result || !result.ok) {
|
|
34979
35093
|
this.user = null;
|
|
34980
35094
|
this.userErrors = null;
|
|
@@ -35001,11 +35115,7 @@ class Model {
|
|
|
35001
35115
|
if (!user)
|
|
35002
35116
|
return;
|
|
35003
35117
|
try {
|
|
35004
|
-
const result = await
|
|
35005
|
-
method: "POST",
|
|
35006
|
-
url: "/saveuserprefs",
|
|
35007
|
-
body: user
|
|
35008
|
-
});
|
|
35118
|
+
const result = await this.post("/saveuserprefs", user);
|
|
35009
35119
|
if (result === null || result === void 0 ? void 0 : result.ok) {
|
|
35010
35120
|
// User preferences modified successfully on the server:
|
|
35011
35121
|
// update the state variables that we're caching
|
|
@@ -35419,8 +35529,10 @@ class Actions {
|
|
|
35419
35529
|
}
|
|
35420
35530
|
async markFavorite(userId, status) {
|
|
35421
35531
|
// Mark or de-mark a user as a favorite
|
|
35532
|
+
if (!this.model.state)
|
|
35533
|
+
return;
|
|
35422
35534
|
try {
|
|
35423
|
-
await request({
|
|
35535
|
+
await request(this.model.state, {
|
|
35424
35536
|
method: "POST",
|
|
35425
35537
|
url: "/favorite",
|
|
35426
35538
|
body: { destuser: userId, action: status ? "add" : "delete" }
|
|
@@ -35438,8 +35550,10 @@ class Actions {
|
|
|
35438
35550
|
async handleChallenge(parameters) {
|
|
35439
35551
|
var _a;
|
|
35440
35552
|
// Reject or retract a challenge
|
|
35553
|
+
if (!this.model.state)
|
|
35554
|
+
return;
|
|
35441
35555
|
try {
|
|
35442
|
-
const json = await request({
|
|
35556
|
+
const json = await request(this.model.state, {
|
|
35443
35557
|
method: "POST",
|
|
35444
35558
|
url: "/challenge",
|
|
35445
35559
|
body: parameters
|
|
@@ -35494,6 +35608,8 @@ class Actions {
|
|
|
35494
35608
|
async startNewGame(oppid, reverse = false) {
|
|
35495
35609
|
var _a;
|
|
35496
35610
|
// Ask the server to initiate a new game against the given opponent
|
|
35611
|
+
if (!this.model.state)
|
|
35612
|
+
return;
|
|
35497
35613
|
try {
|
|
35498
35614
|
const rqBody = { opp: oppid, rev: reverse };
|
|
35499
35615
|
if (this.model.isExplo) {
|
|
@@ -35506,7 +35622,7 @@ class Actions {
|
|
|
35506
35622
|
url: "/initgame",
|
|
35507
35623
|
body: rqBody
|
|
35508
35624
|
};
|
|
35509
|
-
const json = await request(rq);
|
|
35625
|
+
const json = await request(this.model.state, rq);
|
|
35510
35626
|
if (json === null || json === void 0 ? void 0 : json.ok) {
|
|
35511
35627
|
// Log the new game event
|
|
35512
35628
|
const locale = ((_a = this.model.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
@@ -35572,8 +35688,10 @@ class Actions {
|
|
|
35572
35688
|
// User Preference Management Actions
|
|
35573
35689
|
async setUserPref(pref) {
|
|
35574
35690
|
// Set a user preference on the server
|
|
35691
|
+
if (!this.model.state)
|
|
35692
|
+
return;
|
|
35575
35693
|
try {
|
|
35576
|
-
await request({
|
|
35694
|
+
await request(this.model.state, {
|
|
35577
35695
|
method: "POST",
|
|
35578
35696
|
url: "/setuserpref",
|
|
35579
35697
|
body: pref
|
|
@@ -35590,7 +35708,7 @@ class Actions {
|
|
|
35590
35708
|
if (!user || !state)
|
|
35591
35709
|
return false;
|
|
35592
35710
|
try {
|
|
35593
|
-
const json = await request({
|
|
35711
|
+
const json = await request(state, {
|
|
35594
35712
|
method: "POST",
|
|
35595
35713
|
url: "/cancelplan",
|
|
35596
35714
|
body: {}
|
|
@@ -35708,41 +35826,25 @@ async function main$1(state, container) {
|
|
|
35708
35826
|
console.error("No container element found");
|
|
35709
35827
|
return "error";
|
|
35710
35828
|
}
|
|
35711
|
-
// Set up Netskrafl backend server URLs
|
|
35712
|
-
setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
|
|
35713
35829
|
try {
|
|
35714
|
-
|
|
35715
|
-
|
|
35716
|
-
|
|
35717
|
-
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35721
|
-
|
|
35722
|
-
|
|
35723
|
-
state.userNick = loginData.nickname || state.userNick;
|
|
35724
|
-
// Log in to Firebase with the token passed from the server
|
|
35725
|
-
await loginFirebase(state, loginData.firebase_token);
|
|
35726
|
-
// Everything looks OK:
|
|
35727
|
-
// Create the model, actions and view objects in proper sequence
|
|
35728
|
-
const settings = getSettings();
|
|
35729
|
-
const model = new Model(settings, state);
|
|
35730
|
-
const actions = new Actions(model);
|
|
35731
|
-
const view = new View(actions);
|
|
35732
|
-
// Run the Mithril router
|
|
35733
|
-
const routeResolver = createRouteResolver(actions, view);
|
|
35734
|
-
m.route(container, settings.defaultRoute, routeResolver);
|
|
35735
|
-
return "success";
|
|
35736
|
-
}
|
|
35830
|
+
// Skip initial login - authentication will happen lazily on first API call
|
|
35831
|
+
// Create the model, actions and view objects in proper sequence
|
|
35832
|
+
const settings = getSettings();
|
|
35833
|
+
const model = new Model(settings, state);
|
|
35834
|
+
const actions = new Actions(model);
|
|
35835
|
+
const view = new View(actions);
|
|
35836
|
+
// Run the Mithril router
|
|
35837
|
+
const routeResolver = createRouteResolver(actions, view);
|
|
35838
|
+
m.route(container, settings.defaultRoute, routeResolver);
|
|
35737
35839
|
}
|
|
35738
35840
|
catch (e) {
|
|
35739
|
-
console.error("Exception during
|
|
35841
|
+
console.error("Exception during initialization: ", e);
|
|
35842
|
+
return "error";
|
|
35740
35843
|
}
|
|
35741
|
-
|
|
35742
|
-
return "error";
|
|
35844
|
+
return "success";
|
|
35743
35845
|
}
|
|
35744
35846
|
|
|
35745
|
-
const mountForUser$1 = async (state
|
|
35847
|
+
const mountForUser$1 = async (state) => {
|
|
35746
35848
|
// Return a DOM tree containing a mounted Netskrafl UI
|
|
35747
35849
|
// for the user specified in the state object
|
|
35748
35850
|
const { userEmail } = state;
|
|
@@ -35770,18 +35872,12 @@ const mountForUser$1 = async (state, tokenExpired) => {
|
|
|
35770
35872
|
root.className = "netskrafl-user";
|
|
35771
35873
|
return root;
|
|
35772
35874
|
}
|
|
35773
|
-
else if (loginResult === "expired") {
|
|
35774
|
-
// We need a new token from the Málstaður backend
|
|
35775
|
-
root.className = "netskrafl-expired";
|
|
35776
|
-
tokenExpired && tokenExpired(); // This causes a reload of the component
|
|
35777
|
-
return root;
|
|
35778
|
-
}
|
|
35779
35875
|
// console.error("Failed to mount Netskrafl UI for user", userEmail);
|
|
35780
35876
|
throw new Error("Failed to mount Netskrafl UI");
|
|
35781
35877
|
};
|
|
35782
35878
|
const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
35783
35879
|
const ref = React.createRef();
|
|
35784
|
-
const completeState = { ...
|
|
35880
|
+
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
35785
35881
|
const { userEmail } = completeState;
|
|
35786
35882
|
/*
|
|
35787
35883
|
useEffect(() => {
|
|
@@ -35814,7 +35910,7 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
|
35814
35910
|
return;
|
|
35815
35911
|
}
|
|
35816
35912
|
try {
|
|
35817
|
-
mountForUser$1(completeState
|
|
35913
|
+
mountForUser$1(completeState).then((div) => {
|
|
35818
35914
|
// Attach the div as a child of the container
|
|
35819
35915
|
// instead of any previous children
|
|
35820
35916
|
const container = ref.current;
|
|
@@ -35924,8 +36020,8 @@ const GataDagsinsBoardAndRack = {
|
|
|
35924
36020
|
// If no riddle is available, return an empty div
|
|
35925
36021
|
return m(".gatadagsins-board-rack-wrapper", "");
|
|
35926
36022
|
}
|
|
35927
|
-
// Is the move currently on the board the best possible move?
|
|
35928
|
-
const celebrate = (riddle.currentScore || 0) >= riddle.bestPossibleScore;
|
|
36023
|
+
// Is the move currently on the board valid, and the best possible move?
|
|
36024
|
+
const celebrate = riddle.wordGood && (riddle.currentScore || 0) >= riddle.bestPossibleScore;
|
|
35929
36025
|
return m(".gatadagsins-board-rack-wrapper", [
|
|
35930
36026
|
// Board area (top)
|
|
35931
36027
|
m(".gatadagsins-board-area" + (celebrate ? ".celebrate" : ""), [
|
|
@@ -36374,44 +36470,28 @@ async function main(state, container) {
|
|
|
36374
36470
|
console.error("No container element found");
|
|
36375
36471
|
return "error";
|
|
36376
36472
|
}
|
|
36377
|
-
// Set up Netskrafl backend server URLs
|
|
36378
|
-
setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
|
|
36379
36473
|
try {
|
|
36380
|
-
|
|
36381
|
-
|
|
36382
|
-
|
|
36383
|
-
|
|
36384
|
-
|
|
36385
|
-
|
|
36386
|
-
|
|
36387
|
-
|
|
36388
|
-
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
36392
|
-
// Everything looks OK:
|
|
36393
|
-
// Create the model, view and actions objects
|
|
36394
|
-
const settings = getSettings();
|
|
36395
|
-
const model = new Model(settings, state);
|
|
36396
|
-
const actions = new Actions(model);
|
|
36397
|
-
const view = new View(actions);
|
|
36398
|
-
const today = new Date().toISOString().split("T")[0];
|
|
36399
|
-
const locale = state.locale || "is_IS";
|
|
36400
|
-
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36401
|
-
m.mount(container, {
|
|
36402
|
-
view: () => m(GataDagsins$1, { view, date: today, locale }),
|
|
36403
|
-
});
|
|
36404
|
-
return "success";
|
|
36405
|
-
}
|
|
36474
|
+
// Skip initial login - authentication will happen lazily on first API call
|
|
36475
|
+
// Create the model, view and actions objects
|
|
36476
|
+
const settings = getSettings();
|
|
36477
|
+
const model = new Model(settings, state);
|
|
36478
|
+
const actions = new Actions(model);
|
|
36479
|
+
const view = new View(actions);
|
|
36480
|
+
const today = new Date().toISOString().split("T")[0];
|
|
36481
|
+
const locale = state.locale || "is_IS";
|
|
36482
|
+
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36483
|
+
m.mount(container, {
|
|
36484
|
+
view: () => m(GataDagsins$1, { view, date: today, locale }),
|
|
36485
|
+
});
|
|
36406
36486
|
}
|
|
36407
36487
|
catch (e) {
|
|
36408
|
-
console.error("Exception during
|
|
36488
|
+
console.error("Exception during initialization: ", e);
|
|
36489
|
+
return "error";
|
|
36409
36490
|
}
|
|
36410
|
-
|
|
36411
|
-
return "error";
|
|
36491
|
+
return "success";
|
|
36412
36492
|
}
|
|
36413
36493
|
|
|
36414
|
-
const mountForUser = async (state
|
|
36494
|
+
const mountForUser = async (state) => {
|
|
36415
36495
|
// Return a DOM tree containing a mounted Gáta Dagsins UI
|
|
36416
36496
|
// for the user specified in the state object
|
|
36417
36497
|
const { userEmail } = state;
|
|
@@ -36439,18 +36519,12 @@ const mountForUser = async (state, tokenExpired) => {
|
|
|
36439
36519
|
root.className = "gatadagsins-user";
|
|
36440
36520
|
return root;
|
|
36441
36521
|
}
|
|
36442
|
-
else if (loginResult === "expired") {
|
|
36443
|
-
// We need a new token from the Málstaður backend
|
|
36444
|
-
root.className = "gatadagsins-expired";
|
|
36445
|
-
tokenExpired && tokenExpired(); // This causes a reload of the component
|
|
36446
|
-
return root;
|
|
36447
|
-
}
|
|
36448
36522
|
// console.error("Failed to mount Gáta Dagsins UI for user", userEmail);
|
|
36449
36523
|
throw new Error("Failed to mount Gáta Dagsins UI");
|
|
36450
36524
|
};
|
|
36451
36525
|
const GataDagsinsImpl = ({ state, tokenExpired }) => {
|
|
36452
36526
|
const ref = React.createRef();
|
|
36453
|
-
const completeState = { ...
|
|
36527
|
+
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
36454
36528
|
const { userEmail } = completeState;
|
|
36455
36529
|
useEffect(() => {
|
|
36456
36530
|
var _a;
|
|
@@ -36468,7 +36542,7 @@ const GataDagsinsImpl = ({ state, tokenExpired }) => {
|
|
|
36468
36542
|
return;
|
|
36469
36543
|
}
|
|
36470
36544
|
try {
|
|
36471
|
-
mountForUser(completeState
|
|
36545
|
+
mountForUser(completeState).then((div) => {
|
|
36472
36546
|
// Attach the div as a child of the container
|
|
36473
36547
|
// instead of any previous children
|
|
36474
36548
|
const container = ref.current;
|