@mideind/netskrafl-react 1.0.0-beta.9 → 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 +899 -826
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +2 -1
- package/dist/esm/index.js +899 -826
- 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,53 +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
|
-
|
|
2234
|
-
|
|
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"
|
|
2235
2434
|
};
|
|
2236
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
|
+
|
|
2237
2690
|
/**
|
|
2238
2691
|
* @license
|
|
2239
2692
|
* Copyright 2017 Google LLC
|
|
@@ -26559,33 +27012,46 @@ let database;
|
|
|
26559
27012
|
let analytics;
|
|
26560
27013
|
function initFirebase(state) {
|
|
26561
27014
|
try {
|
|
26562
|
-
const projectId = state
|
|
27015
|
+
const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
|
|
26563
27016
|
const firebaseOptions = {
|
|
26564
27017
|
projectId,
|
|
26565
|
-
apiKey:
|
|
27018
|
+
apiKey: firebaseAPIKey,
|
|
26566
27019
|
authDomain: `${projectId}.firebaseapp.com`,
|
|
26567
|
-
databaseURL
|
|
27020
|
+
databaseURL,
|
|
26568
27021
|
storageBucket: `${projectId}.firebasestorage.app`,
|
|
26569
|
-
messagingSenderId:
|
|
26570
|
-
appId:
|
|
26571
|
-
measurementId
|
|
27022
|
+
messagingSenderId: firebaseSenderId,
|
|
27023
|
+
appId: firebaseAppId,
|
|
27024
|
+
measurementId,
|
|
26572
27025
|
};
|
|
26573
|
-
app = initializeApp(firebaseOptions,
|
|
27026
|
+
app = initializeApp(firebaseOptions, projectId);
|
|
26574
27027
|
if (!app) {
|
|
26575
27028
|
console.error("Failed to initialize Firebase");
|
|
27029
|
+
return false;
|
|
26576
27030
|
}
|
|
26577
27031
|
}
|
|
26578
27032
|
catch (e) {
|
|
26579
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);
|
|
26580
27046
|
}
|
|
27047
|
+
if (!auth)
|
|
27048
|
+
return false;
|
|
27049
|
+
return auth.currentUser !== null;
|
|
26581
27050
|
}
|
|
26582
27051
|
async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
26583
|
-
if (!app)
|
|
26584
|
-
initFirebase(state);
|
|
26585
|
-
if (!app)
|
|
27052
|
+
if (!app && !initFirebase(state))
|
|
26586
27053
|
return;
|
|
26587
|
-
const userId = state
|
|
26588
|
-
const locale = state.locale;
|
|
27054
|
+
const { userId, locale, projectId, loginMethod } = state;
|
|
26589
27055
|
auth = getAuth(app);
|
|
26590
27056
|
if (!auth) {
|
|
26591
27057
|
console.error("Failed to initialize Firebase Auth");
|
|
@@ -26598,16 +27064,16 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
|
26598
27064
|
// For new users, log an additional signup event
|
|
26599
27065
|
if (state.newUser) {
|
|
26600
27066
|
logEvent("sign_up", {
|
|
26601
|
-
locale
|
|
26602
|
-
method:
|
|
26603
|
-
userid:
|
|
27067
|
+
locale,
|
|
27068
|
+
method: loginMethod,
|
|
27069
|
+
userid: userId
|
|
26604
27070
|
});
|
|
26605
27071
|
}
|
|
26606
27072
|
// And always log a login event
|
|
26607
27073
|
logEvent("login", {
|
|
26608
|
-
locale
|
|
26609
|
-
method:
|
|
26610
|
-
userid:
|
|
27074
|
+
locale,
|
|
27075
|
+
method: loginMethod,
|
|
27076
|
+
userid: userId
|
|
26611
27077
|
});
|
|
26612
27078
|
}
|
|
26613
27079
|
});
|
|
@@ -26622,7 +27088,7 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
|
26622
27088
|
if (!analytics) {
|
|
26623
27089
|
console.error("Failed to initialize Firebase Analytics");
|
|
26624
27090
|
}
|
|
26625
|
-
initPresence(
|
|
27091
|
+
initPresence(projectId, userId, locale);
|
|
26626
27092
|
}
|
|
26627
27093
|
function initPresence(projectId, userId, locale) {
|
|
26628
27094
|
// Ensure that this user connection is recorded in Firebase
|
|
@@ -26679,473 +27145,6 @@ function logEvent(ev, params) {
|
|
|
26679
27145
|
logEvent$2(analytics, ev, params);
|
|
26680
27146
|
}
|
|
26681
27147
|
|
|
26682
|
-
/*
|
|
26683
|
-
|
|
26684
|
-
i8n.ts
|
|
26685
|
-
|
|
26686
|
-
Single page UI for Netskrafl/Explo using the Mithril library
|
|
26687
|
-
|
|
26688
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26689
|
-
Author: Vilhjálmur Þorsteinsson
|
|
26690
|
-
|
|
26691
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26692
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26693
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26694
|
-
|
|
26695
|
-
|
|
26696
|
-
This module contains internationalization (i18n) utility functions,
|
|
26697
|
-
allowing for translation of displayed text between languages.
|
|
26698
|
-
|
|
26699
|
-
Text messages for individual locales are loaded from the
|
|
26700
|
-
/static/assets/messages.json file, which is fetched from the server.
|
|
26701
|
-
|
|
26702
|
-
*/
|
|
26703
|
-
// Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
|
|
26704
|
-
// This is overwritten in setLocale()
|
|
26705
|
-
let currentLocale = "is_IS";
|
|
26706
|
-
let currentFallback = "is";
|
|
26707
|
-
// Regex that matches embedded interpolations such as "Welcome, {username}!"
|
|
26708
|
-
// Interpolation identifiers should only contain ASCII characters, digits and '_'
|
|
26709
|
-
const rex = /{\s*(\w+)\s*}/g;
|
|
26710
|
-
let messages = {};
|
|
26711
|
-
let messagesLoaded = false;
|
|
26712
|
-
function hasAnyTranslation(msgs, locale) {
|
|
26713
|
-
// Return true if any translation is available for the given locale
|
|
26714
|
-
for (let key in msgs) {
|
|
26715
|
-
if (msgs[key][locale] !== undefined)
|
|
26716
|
-
return true;
|
|
26717
|
-
}
|
|
26718
|
-
return false;
|
|
26719
|
-
}
|
|
26720
|
-
function setLocale(locale, msgs) {
|
|
26721
|
-
// Set the current i18n locale and fallback
|
|
26722
|
-
currentLocale = locale;
|
|
26723
|
-
currentFallback = locale.split("_")[0];
|
|
26724
|
-
// For unsupported locales, i.e. locales that have no
|
|
26725
|
-
// translations available for them, fall back to English (U.S.).
|
|
26726
|
-
if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
|
|
26727
|
-
currentLocale = "en_US";
|
|
26728
|
-
currentFallback = "en";
|
|
26729
|
-
}
|
|
26730
|
-
// Flatten the Messages structure, enabling long strings
|
|
26731
|
-
// to be represented as string arrays in the messages.json file
|
|
26732
|
-
messages = {};
|
|
26733
|
-
for (let key in msgs) {
|
|
26734
|
-
for (let lc in msgs[key]) {
|
|
26735
|
-
let s = msgs[key][lc];
|
|
26736
|
-
if (Array.isArray(s))
|
|
26737
|
-
s = s.join("");
|
|
26738
|
-
if (messages[key] === undefined)
|
|
26739
|
-
messages[key] = {};
|
|
26740
|
-
// If the string s contains HTML markup of the form <tag>...</tag>,
|
|
26741
|
-
// convert it into a list of Mithril Vnode children corresponding to
|
|
26742
|
-
// the text and the tags
|
|
26743
|
-
if (s.match(/<[a-z]+>/)) {
|
|
26744
|
-
// Looks like the string contains HTML markup
|
|
26745
|
-
const vnodes = [];
|
|
26746
|
-
let i = 0;
|
|
26747
|
-
let tagMatch = null;
|
|
26748
|
-
while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
|
|
26749
|
-
// Found what looks like an HTML tag
|
|
26750
|
-
// Calculate the index of the enclosed text within s
|
|
26751
|
-
const tag = tagMatch[0];
|
|
26752
|
-
let j = i + tagMatch.index + tag.length;
|
|
26753
|
-
// Find the end tag
|
|
26754
|
-
let end = s.indexOf("</" + tag.slice(1), j);
|
|
26755
|
-
if (end < 0) {
|
|
26756
|
-
// No end tag - skip past this weirdness
|
|
26757
|
-
i = j;
|
|
26758
|
-
continue;
|
|
26759
|
-
}
|
|
26760
|
-
// Add the text preceding the tag
|
|
26761
|
-
if (tagMatch.index > 0)
|
|
26762
|
-
vnodes.push(s.slice(i, i + tagMatch.index));
|
|
26763
|
-
// Create the Mithril node corresponding to the tag and the enclosed text
|
|
26764
|
-
// and add it to the list
|
|
26765
|
-
vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
|
|
26766
|
-
// Advance the index past the end of the tag
|
|
26767
|
-
i = end + tag.length + 1;
|
|
26768
|
-
}
|
|
26769
|
-
// Push the final text part, if any
|
|
26770
|
-
if (i < s.length)
|
|
26771
|
-
vnodes.push(s.slice(i));
|
|
26772
|
-
// Reassign s to the list of vnodes
|
|
26773
|
-
s = vnodes;
|
|
26774
|
-
}
|
|
26775
|
-
messages[key][lc] = s;
|
|
26776
|
-
}
|
|
26777
|
-
}
|
|
26778
|
-
messagesLoaded = true;
|
|
26779
|
-
}
|
|
26780
|
-
async function loadMessages(locale) {
|
|
26781
|
-
// Load the internationalization message JSON file from the server
|
|
26782
|
-
// and set the user's locale
|
|
26783
|
-
try {
|
|
26784
|
-
const messages = await request({
|
|
26785
|
-
method: "GET",
|
|
26786
|
-
url: "/static/assets/messages.json",
|
|
26787
|
-
withCredentials: false, // Cookies are not allowed for CORS request
|
|
26788
|
-
});
|
|
26789
|
-
setLocale(locale, messages);
|
|
26790
|
-
}
|
|
26791
|
-
catch (_a) {
|
|
26792
|
-
setLocale(locale, {});
|
|
26793
|
-
}
|
|
26794
|
-
}
|
|
26795
|
-
function t(key, ips = {}) {
|
|
26796
|
-
// Main text translation function, supporting interpolation
|
|
26797
|
-
// and HTML tag substitution
|
|
26798
|
-
const msgDict = messages[key];
|
|
26799
|
-
if (msgDict === undefined)
|
|
26800
|
-
// No dictionary for this key - may actually be a missing entry
|
|
26801
|
-
return messagesLoaded ? key : "";
|
|
26802
|
-
// Lookup exact locale, then fallback, then resort to returning the key
|
|
26803
|
-
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
26804
|
-
// If we have an interpolation object, do the interpolation first
|
|
26805
|
-
return Object.keys(ips).length ? interpolate(message, ips) : message;
|
|
26806
|
-
}
|
|
26807
|
-
function ts(key, ips = {}) {
|
|
26808
|
-
// String translation function, supporting interpolation
|
|
26809
|
-
// but not HTML tag substitution
|
|
26810
|
-
const msgDict = messages[key];
|
|
26811
|
-
if (msgDict === undefined)
|
|
26812
|
-
// No dictionary for this key - may actually be a missing entry
|
|
26813
|
-
return messagesLoaded ? key : "";
|
|
26814
|
-
// Lookup exact locale, then fallback, then resort to returning the key
|
|
26815
|
-
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
26816
|
-
if (typeof message != "string")
|
|
26817
|
-
// This is actually an error - the client should be calling t() instead
|
|
26818
|
-
return "";
|
|
26819
|
-
// If we have an interpolation object, do the interpolation first
|
|
26820
|
-
return Object.keys(ips).length ? interpolate_string(message, ips) : message;
|
|
26821
|
-
}
|
|
26822
|
-
function mt(cls, children) {
|
|
26823
|
-
// Wrapper for the Mithril m() function that auto-translates
|
|
26824
|
-
// string and array arguments
|
|
26825
|
-
if (typeof children == "string") {
|
|
26826
|
-
return m(cls, t(children));
|
|
26827
|
-
}
|
|
26828
|
-
if (Array.isArray(children)) {
|
|
26829
|
-
return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
|
|
26830
|
-
}
|
|
26831
|
-
return m(cls, children);
|
|
26832
|
-
}
|
|
26833
|
-
function interpolate(message, ips) {
|
|
26834
|
-
// Replace interpolation placeholders with their corresponding values
|
|
26835
|
-
if (typeof message == "string") {
|
|
26836
|
-
return message.replace(rex, (match, key) => ips[key] || match);
|
|
26837
|
-
}
|
|
26838
|
-
if (Array.isArray(message)) {
|
|
26839
|
-
return message.map((item) => interpolate(item, ips));
|
|
26840
|
-
}
|
|
26841
|
-
return message;
|
|
26842
|
-
}
|
|
26843
|
-
function interpolate_string(message, ips) {
|
|
26844
|
-
// Replace interpolation placeholders with their corresponding values
|
|
26845
|
-
return message.replace(rex, (match, key) => ips[key] || match);
|
|
26846
|
-
}
|
|
26847
|
-
|
|
26848
|
-
/*
|
|
26849
|
-
|
|
26850
|
-
Types.ts
|
|
26851
|
-
|
|
26852
|
-
Common type definitions for the Explo/Netskrafl user interface
|
|
26853
|
-
|
|
26854
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26855
|
-
Author: Vilhjalmur Thorsteinsson
|
|
26856
|
-
|
|
26857
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26858
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26859
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26860
|
-
|
|
26861
|
-
*/
|
|
26862
|
-
// Global constants
|
|
26863
|
-
const RACK_SIZE = 7;
|
|
26864
|
-
const ROWIDS = "ABCDEFGHIJKLMNO";
|
|
26865
|
-
const BOARD_SIZE = ROWIDS.length;
|
|
26866
|
-
const EXTRA_WIDE_LETTERS = "q";
|
|
26867
|
-
const WIDE_LETTERS = "zxmæ";
|
|
26868
|
-
const ZOOM_FACTOR = 1.5;
|
|
26869
|
-
const ERROR_MESSAGES = {
|
|
26870
|
-
// Translations are found in /static/assets/messages.json
|
|
26871
|
-
1: "Enginn stafur lagður niður",
|
|
26872
|
-
2: "Fyrsta orð verður að liggja um byrjunarreitinn",
|
|
26873
|
-
3: "Orð verður að vera samfellt á borðinu",
|
|
26874
|
-
4: "Orð verður að tengjast orði sem fyrir er",
|
|
26875
|
-
5: "Reitur þegar upptekinn",
|
|
26876
|
-
6: "Ekki má vera eyða í orði",
|
|
26877
|
-
7: "word_not_found",
|
|
26878
|
-
8: "word_not_found",
|
|
26879
|
-
9: "Of margir stafir lagðir niður",
|
|
26880
|
-
10: "Stafur er ekki í rekkanum",
|
|
26881
|
-
11: "Of fáir stafir eftir, skipting ekki leyfð",
|
|
26882
|
-
12: "Of mörgum stöfum skipt",
|
|
26883
|
-
13: "Leik vantar á borðið - notið F5/Refresh",
|
|
26884
|
-
14: "Notandi ekki innskráður - notið F5/Refresh",
|
|
26885
|
-
15: "Rangur eða óþekktur notandi",
|
|
26886
|
-
16: "Viðureign finnst ekki",
|
|
26887
|
-
17: "Viðureign er ekki utan tímamarka",
|
|
26888
|
-
18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
|
|
26889
|
-
19: "Véfenging er ekki möguleg í þessari viðureign",
|
|
26890
|
-
20: "Síðasti leikur er ekki véfengjanlegur",
|
|
26891
|
-
21: "Aðeins véfenging eða pass leyfileg",
|
|
26892
|
-
"server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
|
|
26893
|
-
};
|
|
26894
|
-
|
|
26895
|
-
/*
|
|
26896
|
-
|
|
26897
|
-
Util.ts
|
|
26898
|
-
|
|
26899
|
-
Utility functions for the Explo/Netskrafl user interface
|
|
26900
|
-
|
|
26901
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26902
|
-
Author: Vilhjálmur Þorsteinsson
|
|
26903
|
-
|
|
26904
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26905
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26906
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26907
|
-
|
|
26908
|
-
The following code is based on
|
|
26909
|
-
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
|
|
26910
|
-
|
|
26911
|
-
*/
|
|
26912
|
-
// Global vars to cache event state
|
|
26913
|
-
var evCache = [];
|
|
26914
|
-
var origDistance = -1;
|
|
26915
|
-
const PINCH_THRESHOLD = 10; // Minimum pinch movement
|
|
26916
|
-
var hasZoomed = false;
|
|
26917
|
-
// Old-style (non-single-page) game URL prefix
|
|
26918
|
-
const BOARD_PREFIX = "/board?game=";
|
|
26919
|
-
const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
|
|
26920
|
-
function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
|
|
26921
|
-
// Install event handlers for the pointer target
|
|
26922
|
-
attrs.onpointerdown = pointerdown_handler;
|
|
26923
|
-
attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
|
|
26924
|
-
// Use same handler for pointer{up,cancel,out,leave} events since
|
|
26925
|
-
// the semantics for these events - in this app - are the same.
|
|
26926
|
-
attrs.onpointerup = pointerup_handler;
|
|
26927
|
-
attrs.onpointercancel = pointerup_handler;
|
|
26928
|
-
attrs.onpointerout = pointerup_handler;
|
|
26929
|
-
attrs.onpointerleave = pointerup_handler;
|
|
26930
|
-
}
|
|
26931
|
-
function pointerdown_handler(ev) {
|
|
26932
|
-
// The pointerdown event signals the start of a touch interaction.
|
|
26933
|
-
// This event is cached to support 2-finger gestures
|
|
26934
|
-
evCache.push(ev);
|
|
26935
|
-
}
|
|
26936
|
-
function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
|
|
26937
|
-
// This function implements a 2-pointer horizontal pinch/zoom gesture.
|
|
26938
|
-
//
|
|
26939
|
-
// If the distance between the two pointers has increased (zoom in),
|
|
26940
|
-
// the target element's background is changed to "pink" and if the
|
|
26941
|
-
// distance is decreasing (zoom out), the color is changed to "lightblue".
|
|
26942
|
-
//
|
|
26943
|
-
// Find this event in the cache and update its record with this event
|
|
26944
|
-
for (let i = 0; i < evCache.length; i++) {
|
|
26945
|
-
if (ev.pointerId === evCache[i].pointerId) {
|
|
26946
|
-
evCache[i] = ev;
|
|
26947
|
-
break;
|
|
26948
|
-
}
|
|
26949
|
-
}
|
|
26950
|
-
// If two pointers are down, check for pinch gestures
|
|
26951
|
-
if (evCache.length == 2) {
|
|
26952
|
-
// Calculate the distance between the two pointers
|
|
26953
|
-
const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
|
|
26954
|
-
Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
|
|
26955
|
-
if (origDistance > 0) {
|
|
26956
|
-
if (curDistance - origDistance >= PINCH_THRESHOLD) {
|
|
26957
|
-
// The distance between the two pointers has increased
|
|
26958
|
-
if (!hasZoomed)
|
|
26959
|
-
funcZoomIn();
|
|
26960
|
-
hasZoomed = true;
|
|
26961
|
-
}
|
|
26962
|
-
else if (origDistance - curDistance >= PINCH_THRESHOLD) {
|
|
26963
|
-
// The distance between the two pointers has decreased
|
|
26964
|
-
if (!hasZoomed)
|
|
26965
|
-
funcZoomOut();
|
|
26966
|
-
hasZoomed = true;
|
|
26967
|
-
}
|
|
26968
|
-
}
|
|
26969
|
-
else if (origDistance < 0) {
|
|
26970
|
-
// Note the original difference between two pointers
|
|
26971
|
-
origDistance = curDistance;
|
|
26972
|
-
hasZoomed = false;
|
|
26973
|
-
}
|
|
26974
|
-
}
|
|
26975
|
-
}
|
|
26976
|
-
function pointerup_handler(ev) {
|
|
26977
|
-
// Remove this pointer from the cache and reset the target's
|
|
26978
|
-
// background and border
|
|
26979
|
-
remove_event(ev);
|
|
26980
|
-
// If the number of pointers down is less than two then reset diff tracker
|
|
26981
|
-
if (evCache.length < 2) {
|
|
26982
|
-
origDistance = -1;
|
|
26983
|
-
}
|
|
26984
|
-
}
|
|
26985
|
-
function remove_event(ev) {
|
|
26986
|
-
// Remove this event from the target's cache
|
|
26987
|
-
for (let i = 0; i < evCache.length; i++) {
|
|
26988
|
-
if (evCache[i].pointerId === ev.pointerId) {
|
|
26989
|
-
evCache.splice(i, 1);
|
|
26990
|
-
break;
|
|
26991
|
-
}
|
|
26992
|
-
}
|
|
26993
|
-
}
|
|
26994
|
-
function buttonOver(ev) {
|
|
26995
|
-
const clist = ev.currentTarget.classList;
|
|
26996
|
-
if (clist !== undefined && !clist.contains("disabled"))
|
|
26997
|
-
clist.add("over");
|
|
26998
|
-
ev.redraw = false;
|
|
26999
|
-
}
|
|
27000
|
-
function buttonOut(ev) {
|
|
27001
|
-
const clist = ev.currentTarget.classList;
|
|
27002
|
-
if (clist !== undefined)
|
|
27003
|
-
clist.remove("over");
|
|
27004
|
-
ev.redraw = false;
|
|
27005
|
-
}
|
|
27006
|
-
// Glyphicon utility function: inserts a glyphicon span
|
|
27007
|
-
function glyph(icon, attrs, grayed) {
|
|
27008
|
-
return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
|
|
27009
|
-
}
|
|
27010
|
-
function glyphGrayed(icon, attrs) {
|
|
27011
|
-
return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
|
|
27012
|
-
}
|
|
27013
|
-
// Utility function: inserts non-breaking space
|
|
27014
|
-
function nbsp(n) {
|
|
27015
|
-
return m.trust(" ");
|
|
27016
|
-
}
|
|
27017
|
-
// Utility functions
|
|
27018
|
-
function escapeHtml(string) {
|
|
27019
|
-
/* Utility function to properly encode a string into HTML */
|
|
27020
|
-
const entityMap = {
|
|
27021
|
-
"&": "&",
|
|
27022
|
-
"<": "<",
|
|
27023
|
-
">": ">",
|
|
27024
|
-
'"': '"',
|
|
27025
|
-
"'": ''',
|
|
27026
|
-
"/": '/'
|
|
27027
|
-
};
|
|
27028
|
-
return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
|
|
27029
|
-
}
|
|
27030
|
-
function getUrlVars(url) {
|
|
27031
|
-
// Get values from a URL query string
|
|
27032
|
-
const hashes = url.split('&');
|
|
27033
|
-
const vars = {};
|
|
27034
|
-
for (let i = 0; i < hashes.length; i++) {
|
|
27035
|
-
const hash = hashes[i].split('=');
|
|
27036
|
-
if (hash.length == 2)
|
|
27037
|
-
vars[hash[0]] = decodeURIComponent(hash[1]);
|
|
27038
|
-
}
|
|
27039
|
-
return vars;
|
|
27040
|
-
}
|
|
27041
|
-
function getInput(id) {
|
|
27042
|
-
// Return the current value of a text input field
|
|
27043
|
-
const elem = document.getElementById(id);
|
|
27044
|
-
return elem.value;
|
|
27045
|
-
}
|
|
27046
|
-
function setInput(id, val) {
|
|
27047
|
-
// Set the current value of a text input field
|
|
27048
|
-
const elem = document.getElementById(id);
|
|
27049
|
-
elem.value = val;
|
|
27050
|
-
}
|
|
27051
|
-
function playAudio(elemId) {
|
|
27052
|
-
// Play an audio file
|
|
27053
|
-
const sound = document.getElementById(elemId);
|
|
27054
|
-
if (sound)
|
|
27055
|
-
sound.play();
|
|
27056
|
-
}
|
|
27057
|
-
function arrayEqual(a, b) {
|
|
27058
|
-
// Return true if arrays a and b are equal
|
|
27059
|
-
if (a.length != b.length)
|
|
27060
|
-
return false;
|
|
27061
|
-
for (let i = 0; i < a.length; i++)
|
|
27062
|
-
if (a[i] != b[i])
|
|
27063
|
-
return false;
|
|
27064
|
-
return true;
|
|
27065
|
-
}
|
|
27066
|
-
function gameUrl(url) {
|
|
27067
|
-
// Convert old-style game URL to new-style single-page URL
|
|
27068
|
-
// The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
|
|
27069
|
-
if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
|
|
27070
|
-
// Cut off "/board?game="
|
|
27071
|
-
url = url.slice(BOARD_PREFIX_LEN);
|
|
27072
|
-
// Isolate the game UUID
|
|
27073
|
-
const uuid = url.slice(0, 36);
|
|
27074
|
-
// Isolate the other parameters, if any
|
|
27075
|
-
let params = url.slice(36);
|
|
27076
|
-
// Start parameter section of URL with a ? sign
|
|
27077
|
-
if (params.length > 0 && params.charAt(0) == "&")
|
|
27078
|
-
params = "?" + params.slice(1);
|
|
27079
|
-
// Return the single-page URL, to be consumed by m.route.Link()
|
|
27080
|
-
return "/game/" + uuid + params;
|
|
27081
|
-
}
|
|
27082
|
-
function scrollMovelistToBottom() {
|
|
27083
|
-
// If the length of the move list has changed,
|
|
27084
|
-
// scroll the last move into view
|
|
27085
|
-
let movelist = document.querySelectorAll("div.movelist .move");
|
|
27086
|
-
if (!movelist || !movelist.length)
|
|
27087
|
-
return;
|
|
27088
|
-
let target = movelist[movelist.length - 1];
|
|
27089
|
-
let parent = target.parentNode;
|
|
27090
|
-
let len = parent.getAttribute("data-len");
|
|
27091
|
-
let intLen = (!len) ? 0 : parseInt(len);
|
|
27092
|
-
if (movelist.length > intLen) {
|
|
27093
|
-
// The list has grown since we last updated it:
|
|
27094
|
-
// scroll to the bottom and mark its length
|
|
27095
|
-
parent.scrollTop = target.offsetTop;
|
|
27096
|
-
}
|
|
27097
|
-
parent.setAttribute("data-len", movelist.length.toString());
|
|
27098
|
-
}
|
|
27099
|
-
function coord(row, col, vertical = false) {
|
|
27100
|
-
// Return the co-ordinate string for the given 0-based row and col
|
|
27101
|
-
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
|
|
27102
|
-
return null;
|
|
27103
|
-
// Horizontal moves have the row letter first
|
|
27104
|
-
// Vertical moves have the column number first
|
|
27105
|
-
return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
|
|
27106
|
-
}
|
|
27107
|
-
function toVector(co) {
|
|
27108
|
-
// Convert a co-ordinate string to a 0-based row, col and direction vector
|
|
27109
|
-
var dx = 0, dy = 0;
|
|
27110
|
-
var col = 0;
|
|
27111
|
-
var row = ROWIDS.indexOf(co[0]);
|
|
27112
|
-
if (row >= 0) {
|
|
27113
|
-
/* Horizontal move */
|
|
27114
|
-
col = parseInt(co.slice(1)) - 1;
|
|
27115
|
-
dx = 1;
|
|
27116
|
-
}
|
|
27117
|
-
else {
|
|
27118
|
-
/* Vertical move */
|
|
27119
|
-
row = ROWIDS.indexOf(co.slice(-1));
|
|
27120
|
-
col = parseInt(co) - 1;
|
|
27121
|
-
dy = 1;
|
|
27122
|
-
}
|
|
27123
|
-
return { col: col, row: row, dx: dx, dy: dy };
|
|
27124
|
-
}
|
|
27125
|
-
function valueOrK(value, breakpoint = 10000) {
|
|
27126
|
-
// Return a numeric value as a string, but in kilos (thousands)
|
|
27127
|
-
// if it exceeds a breakpoint, in that case suffixed by "K"
|
|
27128
|
-
const sign = value < 0 ? "-" : "";
|
|
27129
|
-
value = Math.abs(value);
|
|
27130
|
-
if (value < breakpoint)
|
|
27131
|
-
return `${sign}${value}`;
|
|
27132
|
-
value = Math.round(value / 1000);
|
|
27133
|
-
return `${sign}${value}K`;
|
|
27134
|
-
}
|
|
27135
|
-
// SalesCloud stuff
|
|
27136
|
-
function doRegisterSalesCloud(i, s, o, g, r, a, m) {
|
|
27137
|
-
i.SalesCloudObject = r;
|
|
27138
|
-
i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
|
|
27139
|
-
i[r].l = 1 * new Date();
|
|
27140
|
-
a = s.createElement(o);
|
|
27141
|
-
m = s.getElementsByTagName(o)[0];
|
|
27142
|
-
a.src = g;
|
|
27143
|
-
m.parentNode.insertBefore(a, m);
|
|
27144
|
-
}
|
|
27145
|
-
function registerSalesCloud() {
|
|
27146
|
-
doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
|
|
27147
|
-
}
|
|
27148
|
-
|
|
27149
27148
|
/*
|
|
27150
27149
|
|
|
27151
27150
|
Logo.ts
|
|
@@ -27255,7 +27254,7 @@ const NetskraflLegend = (initialVnode) => {
|
|
|
27255
27254
|
m.redraw();
|
|
27256
27255
|
}
|
|
27257
27256
|
return {
|
|
27258
|
-
|
|
27257
|
+
oninit: () => {
|
|
27259
27258
|
if (msStepTime && ival === 0) {
|
|
27260
27259
|
ival = setInterval(doStep, msStepTime);
|
|
27261
27260
|
}
|
|
@@ -27310,10 +27309,197 @@ const AnimatedNetskraflLogo = (initialVnode) => {
|
|
|
27310
27309
|
};
|
|
27311
27310
|
};
|
|
27312
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
|
+
|
|
27313
27499
|
const SPINNER_INITIAL_DELAY = 800; // milliseconds
|
|
27314
27500
|
const Spinner = {
|
|
27315
27501
|
// Show a spinner wait box, after an initial delay
|
|
27316
|
-
|
|
27502
|
+
oninit: (vnode) => {
|
|
27317
27503
|
vnode.state.show = false;
|
|
27318
27504
|
vnode.state.ival = setTimeout(() => {
|
|
27319
27505
|
vnode.state.show = true;
|
|
@@ -27426,11 +27612,12 @@ const OnlinePresence = (initialVnode) => {
|
|
|
27426
27612
|
const askServer = attrs.online === undefined;
|
|
27427
27613
|
const id = attrs.id;
|
|
27428
27614
|
const userId = attrs.userId;
|
|
27615
|
+
const state = attrs.state;
|
|
27429
27616
|
let loading = false;
|
|
27430
27617
|
async function _update() {
|
|
27431
27618
|
if (askServer && !loading) {
|
|
27432
27619
|
loading = true;
|
|
27433
|
-
const json = await request({
|
|
27620
|
+
const json = await request(state, {
|
|
27434
27621
|
method: "POST",
|
|
27435
27622
|
url: "/onlinecheck",
|
|
27436
27623
|
body: { user: userId }
|
|
@@ -27440,7 +27627,7 @@ const OnlinePresence = (initialVnode) => {
|
|
|
27440
27627
|
}
|
|
27441
27628
|
}
|
|
27442
27629
|
return {
|
|
27443
|
-
|
|
27630
|
+
oninit: _update,
|
|
27444
27631
|
view: (vnode) => {
|
|
27445
27632
|
var _a, _b;
|
|
27446
27633
|
if (!askServer)
|
|
@@ -27706,6 +27893,7 @@ const WaitDialog = (initialVnode) => {
|
|
|
27706
27893
|
const attrs = initialVnode.attrs;
|
|
27707
27894
|
const view = attrs.view;
|
|
27708
27895
|
const model = view.model;
|
|
27896
|
+
const state = model.state;
|
|
27709
27897
|
const duration = attrs.duration;
|
|
27710
27898
|
const oppId = attrs.oppId;
|
|
27711
27899
|
const key = attrs.challengeKey;
|
|
@@ -27721,9 +27909,9 @@ const WaitDialog = (initialVnode) => {
|
|
|
27721
27909
|
async function updateOnline() {
|
|
27722
27910
|
// Initiate an online check on the opponent
|
|
27723
27911
|
try {
|
|
27724
|
-
if (!oppId || !key)
|
|
27912
|
+
if (!oppId || !key || !state)
|
|
27725
27913
|
return;
|
|
27726
|
-
const json = await request({
|
|
27914
|
+
const json = await request(state, {
|
|
27727
27915
|
method: "POST",
|
|
27728
27916
|
url: "/initwait",
|
|
27729
27917
|
body: { opp: oppId, key }
|
|
@@ -27739,8 +27927,10 @@ const WaitDialog = (initialVnode) => {
|
|
|
27739
27927
|
}
|
|
27740
27928
|
async function cancelWait() {
|
|
27741
27929
|
// Cancel a pending wait for a timed game
|
|
27930
|
+
if (!state)
|
|
27931
|
+
return;
|
|
27742
27932
|
try {
|
|
27743
|
-
await request({
|
|
27933
|
+
await request(state, {
|
|
27744
27934
|
method: "POST",
|
|
27745
27935
|
url: "/cancelwait",
|
|
27746
27936
|
body: {
|
|
@@ -27774,11 +27964,13 @@ const WaitDialog = (initialVnode) => {
|
|
|
27774
27964
|
return {
|
|
27775
27965
|
oncreate,
|
|
27776
27966
|
view: () => {
|
|
27967
|
+
if (!state)
|
|
27968
|
+
return null;
|
|
27777
27969
|
return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
|
|
27778
27970
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
27779
27971
|
m("td", m("h1.chall-icon", glyph("time"))),
|
|
27780
27972
|
m("td.l-border", [
|
|
27781
|
-
m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline }),
|
|
27973
|
+
m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
|
|
27782
27974
|
m("h1", oppNick),
|
|
27783
27975
|
m("h2", oppName)
|
|
27784
27976
|
])
|
|
@@ -27821,6 +28013,7 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27821
28013
|
// is linked up with her opponent and a new game is started
|
|
27822
28014
|
const attrs = initialVnode.attrs;
|
|
27823
28015
|
const view = attrs.view;
|
|
28016
|
+
const state = view.model.state;
|
|
27824
28017
|
const oppId = attrs.oppId;
|
|
27825
28018
|
const key = attrs.challengeKey;
|
|
27826
28019
|
let oppNick = attrs.oppNick;
|
|
@@ -27828,11 +28021,11 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27828
28021
|
let loading = false;
|
|
27829
28022
|
async function waitCheck() {
|
|
27830
28023
|
// Initiate a wait status check on the opponent
|
|
27831
|
-
if (loading)
|
|
28024
|
+
if (loading || !state)
|
|
27832
28025
|
return; // Already checking
|
|
27833
28026
|
loading = true;
|
|
27834
28027
|
try {
|
|
27835
|
-
const json = await request({
|
|
28028
|
+
const json = await request(state, {
|
|
27836
28029
|
method: "POST",
|
|
27837
28030
|
url: "/waitcheck",
|
|
27838
28031
|
body: { user: oppId, key }
|
|
@@ -27855,7 +28048,7 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27855
28048
|
}
|
|
27856
28049
|
}
|
|
27857
28050
|
return {
|
|
27858
|
-
|
|
28051
|
+
oninit: waitCheck,
|
|
27859
28052
|
view: () => {
|
|
27860
28053
|
return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
|
|
27861
28054
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
@@ -28040,87 +28233,6 @@ const FriendCancelConfirmDialog = (initialVnode) => {
|
|
|
28040
28233
|
};
|
|
28041
28234
|
};
|
|
28042
28235
|
|
|
28043
|
-
/*
|
|
28044
|
-
|
|
28045
|
-
Login.ts
|
|
28046
|
-
|
|
28047
|
-
Login UI for Metskrafl using the Mithril library
|
|
28048
|
-
|
|
28049
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
28050
|
-
Author: Vilhjálmur Þorsteinsson
|
|
28051
|
-
|
|
28052
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
28053
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
28054
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
28055
|
-
|
|
28056
|
-
This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
|
|
28057
|
-
straightforward JavaScript single-page reactive UI library.
|
|
28058
|
-
|
|
28059
|
-
*/
|
|
28060
|
-
const loginUserByEmail = async (email, nickname, fullname, token) => {
|
|
28061
|
-
// Call the /login_malstadur endpoint on the server
|
|
28062
|
-
// to log in the user with the given email and token.
|
|
28063
|
-
// The token is a standard HS256-encoded JWT with aud "netskrafl"
|
|
28064
|
-
// and iss typically "malstadur".
|
|
28065
|
-
return request({
|
|
28066
|
-
method: "POST",
|
|
28067
|
-
url: "/login_malstadur",
|
|
28068
|
-
body: { email, nickname, fullname, token }
|
|
28069
|
-
});
|
|
28070
|
-
};
|
|
28071
|
-
const LoginError = {
|
|
28072
|
-
view: (vnode) => {
|
|
28073
|
-
var _a;
|
|
28074
|
-
return m("div.error", { style: { visibility: "visible" } }, ((_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.message) || "Error logging in");
|
|
28075
|
-
}
|
|
28076
|
-
};
|
|
28077
|
-
const LoginForm = (initialVnode) => {
|
|
28078
|
-
const loginUrl = initialVnode.attrs.loginUrl;
|
|
28079
|
-
let loginInProgress = false;
|
|
28080
|
-
function doLogin(ev) {
|
|
28081
|
-
loginInProgress = true;
|
|
28082
|
-
ev.preventDefault();
|
|
28083
|
-
window.location.href = loginUrl;
|
|
28084
|
-
}
|
|
28085
|
-
return {
|
|
28086
|
-
view: () => {
|
|
28087
|
-
return m.fragment({}, [
|
|
28088
|
-
// This is visible on large screens
|
|
28089
|
-
m("div.loginform-large", [
|
|
28090
|
-
m(NetskraflLogoOnly, {
|
|
28091
|
-
className: "login-logo",
|
|
28092
|
-
width: 200,
|
|
28093
|
-
}),
|
|
28094
|
-
m(NetskraflLegend, {
|
|
28095
|
-
className: "login-legend",
|
|
28096
|
-
width: 600,
|
|
28097
|
-
msStepTime: 0
|
|
28098
|
-
}),
|
|
28099
|
-
mt("div.welcome", "welcome_0"),
|
|
28100
|
-
mt("div.welcome", "welcome_1"),
|
|
28101
|
-
mt("div.welcome", "welcome_2"),
|
|
28102
|
-
m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
|
|
28103
|
-
t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
|
|
28104
|
-
])
|
|
28105
|
-
]),
|
|
28106
|
-
// This is visible on small screens
|
|
28107
|
-
m("div.loginform-small", [
|
|
28108
|
-
m(NetskraflLogoOnly, {
|
|
28109
|
-
className: "login-logo",
|
|
28110
|
-
width: 160,
|
|
28111
|
-
}),
|
|
28112
|
-
m(NetskraflLegend, {
|
|
28113
|
-
className: "login-legend",
|
|
28114
|
-
width: 650,
|
|
28115
|
-
msStepTime: 0
|
|
28116
|
-
}),
|
|
28117
|
-
m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
|
|
28118
|
-
])
|
|
28119
|
-
]);
|
|
28120
|
-
}
|
|
28121
|
-
};
|
|
28122
|
-
};
|
|
28123
|
-
|
|
28124
28236
|
/*
|
|
28125
28237
|
|
|
28126
28238
|
ChallengeDialog.ts
|
|
@@ -28151,7 +28263,11 @@ const ChallengeDialog = () => {
|
|
|
28151
28263
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
28152
28264
|
m("td", m("h1.chall-icon", glyph("hand-right"))),
|
|
28153
28265
|
m("td.l-border", [
|
|
28154
|
-
m(OnlinePresence, {
|
|
28266
|
+
m(OnlinePresence, {
|
|
28267
|
+
id: "chall-online",
|
|
28268
|
+
userId: item.userid,
|
|
28269
|
+
state,
|
|
28270
|
+
}),
|
|
28155
28271
|
m("h1", item.nick),
|
|
28156
28272
|
m("h2", item.fullname)
|
|
28157
28273
|
])
|
|
@@ -29467,7 +29583,7 @@ const PromoDialog = (initialVnode) => {
|
|
|
29467
29583
|
initFunc();
|
|
29468
29584
|
}
|
|
29469
29585
|
return {
|
|
29470
|
-
|
|
29586
|
+
oninit: _fetchContent,
|
|
29471
29587
|
view: (vnode) => {
|
|
29472
29588
|
let initFunc = vnode.attrs.initFunc;
|
|
29473
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", {
|
|
@@ -29539,7 +29655,7 @@ const UserInfoDialog = (initialVnode) => {
|
|
|
29539
29655
|
}
|
|
29540
29656
|
}
|
|
29541
29657
|
return {
|
|
29542
|
-
|
|
29658
|
+
oninit: (vnode) => {
|
|
29543
29659
|
_updateRecentList(vnode);
|
|
29544
29660
|
_updateStats(vnode);
|
|
29545
29661
|
},
|
|
@@ -30529,7 +30645,9 @@ const Board = (initialVnode) => {
|
|
|
30529
30645
|
const scale = view.boardScale || 1.0;
|
|
30530
30646
|
let attrs = {};
|
|
30531
30647
|
// Add handlers for pinch zoom functionality
|
|
30532
|
-
|
|
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());
|
|
30533
30651
|
if (scale !== 1.0)
|
|
30534
30652
|
attrs.style = `transform: scale(${scale})`;
|
|
30535
30653
|
return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
|
|
@@ -30707,7 +30825,7 @@ const Chat = (initialVnode) => {
|
|
|
30707
30825
|
const { view } = initialVnode.attrs;
|
|
30708
30826
|
const model = view.model;
|
|
30709
30827
|
const game = model.game;
|
|
30710
|
-
model.state;
|
|
30828
|
+
const state = model.state;
|
|
30711
30829
|
function decodeTimestamp(ts) {
|
|
30712
30830
|
// Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
|
|
30713
30831
|
return {
|
|
@@ -30764,7 +30882,7 @@ const Chat = (initialVnode) => {
|
|
|
30764
30882
|
for (const emoticon of EMOTICONS)
|
|
30765
30883
|
if (str.indexOf(emoticon.icon) >= 0) {
|
|
30766
30884
|
// The string contains the emoticon: prepare to replace all occurrences
|
|
30767
|
-
const imgUrl = serverUrl(emoticon.image);
|
|
30885
|
+
const imgUrl = serverUrl(state, emoticon.image);
|
|
30768
30886
|
const img = `<img src='${imgUrl}' height='32' width='32'>`;
|
|
30769
30887
|
// Re the following trick, see https://stackoverflow.com/questions/1144783/
|
|
30770
30888
|
// replacing-all-occurrences-of-a-string-in-javascript
|
|
@@ -31915,13 +32033,13 @@ class View {
|
|
|
31915
32033
|
views.push(this.vwLogin());
|
|
31916
32034
|
break;
|
|
31917
32035
|
case "loginerror":
|
|
31918
|
-
views.push(m(LoginError));
|
|
32036
|
+
views.push(m(LoginError, { key: "login-error" }, t("Villa við innskráningu")));
|
|
31919
32037
|
break;
|
|
31920
32038
|
case "main":
|
|
31921
|
-
views.push(m(Main, { view: this }));
|
|
32039
|
+
views.push(m(Main, { key: "main", view: this }));
|
|
31922
32040
|
break;
|
|
31923
32041
|
case "game":
|
|
31924
|
-
views.push(m(GameView, { view: this }));
|
|
32042
|
+
views.push(m(GameView, { key: "game", view: this }));
|
|
31925
32043
|
break;
|
|
31926
32044
|
case "review":
|
|
31927
32045
|
const n = vwReview(this);
|
|
@@ -31929,7 +32047,7 @@ class View {
|
|
|
31929
32047
|
break;
|
|
31930
32048
|
case "thanks":
|
|
31931
32049
|
// Display a thank-you dialog on top of the normal main screen
|
|
31932
|
-
views.push(m(Main, { view: this }));
|
|
32050
|
+
views.push(m(Main, { key: "main", view: this }));
|
|
31933
32051
|
// Be careful to add the Thanks dialog only once to the stack
|
|
31934
32052
|
if (!this.dialogStack.length)
|
|
31935
32053
|
this.showThanks();
|
|
@@ -31940,7 +32058,7 @@ class View {
|
|
|
31940
32058
|
views.push(this.vwHelp(parseInt(m.route.param("tab") || ""), parseInt(m.route.param("faq") || "")));
|
|
31941
32059
|
break;
|
|
31942
32060
|
default:
|
|
31943
|
-
return [m("div", t("Þessi vefslóð er ekki rétt"))];
|
|
32061
|
+
return [m("div", { key: "error" }, t("Þessi vefslóð er ekki rétt"))];
|
|
31944
32062
|
}
|
|
31945
32063
|
// Push any open dialogs
|
|
31946
32064
|
for (const dialog of this.dialogStack) {
|
|
@@ -31955,7 +32073,7 @@ class View {
|
|
|
31955
32073
|
}
|
|
31956
32074
|
// Overlay a spinner, if active
|
|
31957
32075
|
if (model.spinners)
|
|
31958
|
-
views.push(m(Spinner));
|
|
32076
|
+
views.push(m(Spinner, { key: "spinner" }));
|
|
31959
32077
|
return views;
|
|
31960
32078
|
}
|
|
31961
32079
|
// Dialog support
|
|
@@ -32036,7 +32154,7 @@ class View {
|
|
|
32036
32154
|
zoomOut() {
|
|
32037
32155
|
if (this.boardScale !== 1.0) {
|
|
32038
32156
|
this.boardScale = 1.0;
|
|
32039
|
-
setTimeout(this.resetScale);
|
|
32157
|
+
setTimeout(() => this.resetScale());
|
|
32040
32158
|
}
|
|
32041
32159
|
}
|
|
32042
32160
|
resetScale() {
|
|
@@ -32083,7 +32201,7 @@ class View {
|
|
|
32083
32201
|
// No game or we're in full screen mode: always 100% scale
|
|
32084
32202
|
// Also, as soon as a move is being processed by the server, we zoom out
|
|
32085
32203
|
this.boardScale = 1.0; // Needs to be done before setTimeout() call
|
|
32086
|
-
setTimeout(this.resetScale);
|
|
32204
|
+
setTimeout(() => this.resetScale());
|
|
32087
32205
|
return;
|
|
32088
32206
|
}
|
|
32089
32207
|
const tp = game.tilesPlaced();
|
|
@@ -32177,7 +32295,7 @@ class View {
|
|
|
32177
32295
|
}
|
|
32178
32296
|
}
|
|
32179
32297
|
// Output literal HTML obtained from rawhelp.html on the server
|
|
32180
|
-
return m.fragment({}, [
|
|
32298
|
+
return m.fragment({ key: "help" }, [
|
|
32181
32299
|
m(LeftLogo),
|
|
32182
32300
|
m(UserId, { view: this }),
|
|
32183
32301
|
this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
|
|
@@ -32223,6 +32341,7 @@ class View {
|
|
|
32223
32341
|
vnode.dom.querySelector("#nickname").focus();
|
|
32224
32342
|
}
|
|
32225
32343
|
return m(".modal-dialog", {
|
|
32344
|
+
key: "userprefs",
|
|
32226
32345
|
id: "user-dialog",
|
|
32227
32346
|
oncreate: initFocus
|
|
32228
32347
|
// onupdate: initFocus
|
|
@@ -32304,11 +32423,12 @@ class View {
|
|
|
32304
32423
|
model.loadUser(true); // Activate spinner while loading
|
|
32305
32424
|
if (!model.user)
|
|
32306
32425
|
// Nothing to edit (the spinner should be showing in this case)
|
|
32307
|
-
return m.fragment({}, []);
|
|
32426
|
+
return m.fragment({ key: "userprefs-empty" }, []);
|
|
32308
32427
|
return this.vwUserPrefsDialog();
|
|
32309
32428
|
}
|
|
32310
32429
|
vwUserInfo(args) {
|
|
32311
32430
|
return m(UserInfoDialog, {
|
|
32431
|
+
key: "userinfodialog-" + args.userid,
|
|
32312
32432
|
view: this,
|
|
32313
32433
|
userid: args.userid,
|
|
32314
32434
|
nick: args.nick,
|
|
@@ -32317,6 +32437,7 @@ class View {
|
|
|
32317
32437
|
}
|
|
32318
32438
|
vwPromo(args) {
|
|
32319
32439
|
return m(PromoDialog, {
|
|
32440
|
+
key: "promo-" + args.kind,
|
|
32320
32441
|
view: this,
|
|
32321
32442
|
kind: args.kind,
|
|
32322
32443
|
initFunc: args.initFunc
|
|
@@ -32324,6 +32445,7 @@ class View {
|
|
|
32324
32445
|
}
|
|
32325
32446
|
vwWait(args) {
|
|
32326
32447
|
return m(WaitDialog, {
|
|
32448
|
+
key: "wait-" + args.challengeKey,
|
|
32327
32449
|
view: this,
|
|
32328
32450
|
oppId: args.oppId,
|
|
32329
32451
|
oppNick: args.oppNick,
|
|
@@ -32334,6 +32456,7 @@ class View {
|
|
|
32334
32456
|
}
|
|
32335
32457
|
vwAccept(args) {
|
|
32336
32458
|
return m(AcceptDialog, {
|
|
32459
|
+
key: "accept-" + args.challengeKey,
|
|
32337
32460
|
view: this,
|
|
32338
32461
|
oppId: args.oppId,
|
|
32339
32462
|
oppNick: args.oppNick,
|
|
@@ -32344,7 +32467,7 @@ class View {
|
|
|
32344
32467
|
var _a;
|
|
32345
32468
|
const model = this.model;
|
|
32346
32469
|
const loginUrl = ((_a = model.state) === null || _a === void 0 ? void 0 : _a.loginUrl) || "";
|
|
32347
|
-
return m(LoginForm, { loginUrl });
|
|
32470
|
+
return m(LoginForm, { key: "login", loginUrl });
|
|
32348
32471
|
}
|
|
32349
32472
|
vwDialogs() {
|
|
32350
32473
|
// Show prompt dialogs below game board, if any
|
|
@@ -32423,12 +32546,12 @@ class View {
|
|
|
32423
32546
|
View.dialogViews = {
|
|
32424
32547
|
userprefs: (view) => view.vwUserPrefs(),
|
|
32425
32548
|
userinfo: (view, args) => view.vwUserInfo(args),
|
|
32426
|
-
challenge: (view, args) => m(ChallengeDialog, { view, item: args }),
|
|
32549
|
+
challenge: (view, args) => m(ChallengeDialog, { key: "challenge-" + args.item.challenge_key, view, item: args }),
|
|
32427
32550
|
promo: (view, args) => view.vwPromo(args),
|
|
32428
|
-
friend: (view) => m(FriendPromoteDialog, { view }),
|
|
32429
|
-
thanks: (view) => m(FriendThanksDialog, { view }),
|
|
32430
|
-
cancel: (view) => m(FriendCancelDialog, { view }),
|
|
32431
|
-
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 }),
|
|
32432
32555
|
wait: (view, args) => view.vwWait(args),
|
|
32433
32556
|
accept: (view, args) => view.vwAccept(args)
|
|
32434
32557
|
};
|
|
@@ -32466,7 +32589,7 @@ class WordChecker {
|
|
|
32466
32589
|
}
|
|
32467
32590
|
}
|
|
32468
32591
|
}
|
|
32469
|
-
async checkWords(locale, words) {
|
|
32592
|
+
async checkWords(state, locale, words) {
|
|
32470
32593
|
// Return true if all words are valid in the given locale,
|
|
32471
32594
|
// or false otherwise. Lookups are cached for efficiency.
|
|
32472
32595
|
let cache = this.wordCheckCache[locale];
|
|
@@ -32494,7 +32617,7 @@ class WordChecker {
|
|
|
32494
32617
|
}
|
|
32495
32618
|
// We need a server roundtrip
|
|
32496
32619
|
try {
|
|
32497
|
-
const response = await requestMoves({
|
|
32620
|
+
const response = await requestMoves(state, {
|
|
32498
32621
|
url: "/wordcheck",
|
|
32499
32622
|
body: {
|
|
32500
32623
|
locale,
|
|
@@ -32724,7 +32847,7 @@ const BOARD = {
|
|
|
32724
32847
|
}
|
|
32725
32848
|
};
|
|
32726
32849
|
class BaseGame {
|
|
32727
|
-
constructor(uuid, board_type = "standard") {
|
|
32850
|
+
constructor(uuid, state, board_type = "standard") {
|
|
32728
32851
|
// Basic game properties that don't change while the game is underway
|
|
32729
32852
|
this.locale = "is_IS";
|
|
32730
32853
|
this.alphabet = "";
|
|
@@ -32758,6 +32881,7 @@ class BaseGame {
|
|
|
32758
32881
|
this.board_type = board_type;
|
|
32759
32882
|
this.startSquare = START_SQUARE[board_type];
|
|
32760
32883
|
this.startCoord = START_COORD[board_type];
|
|
32884
|
+
this.state = state;
|
|
32761
32885
|
}
|
|
32762
32886
|
// Default init method that can be overridden
|
|
32763
32887
|
init() {
|
|
@@ -32998,7 +33122,7 @@ class BaseGame {
|
|
|
32998
33122
|
if (!this.manual) {
|
|
32999
33123
|
// This is not a manual-wordcheck game:
|
|
33000
33124
|
// Check the word that has been laid down
|
|
33001
|
-
const found = await wordChecker.checkWords(this.locale, scoreResult.words);
|
|
33125
|
+
const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
|
|
33002
33126
|
this.wordGood = found;
|
|
33003
33127
|
this.wordBad = !found;
|
|
33004
33128
|
}
|
|
@@ -33261,10 +33385,10 @@ const MAX_OVERTIME = 10 * 60.0;
|
|
|
33261
33385
|
const DEBUG_OVERTIME = 1 * 60.0;
|
|
33262
33386
|
const GAME_OVER = 99; // Error code corresponding to the Error class in skraflmechanics.py
|
|
33263
33387
|
class Game extends BaseGame {
|
|
33264
|
-
constructor(uuid, srvGame, moveListener, maxOvertime) {
|
|
33388
|
+
constructor(uuid, srvGame, moveListener, state, maxOvertime) {
|
|
33265
33389
|
var _a;
|
|
33266
33390
|
// Call parent constructor
|
|
33267
|
-
super(uuid); // Default board_type: "standard"
|
|
33391
|
+
super(uuid, state); // Default board_type: "standard"
|
|
33268
33392
|
// A class that represents a Netskrafl game instance on the client
|
|
33269
33393
|
// Netskrafl-specific properties
|
|
33270
33394
|
this.userid = ["", ""];
|
|
@@ -33521,7 +33645,7 @@ class Game extends BaseGame {
|
|
|
33521
33645
|
try {
|
|
33522
33646
|
if (!this.uuid)
|
|
33523
33647
|
return;
|
|
33524
|
-
const result = await request({
|
|
33648
|
+
const result = await request(this.state, {
|
|
33525
33649
|
method: "POST",
|
|
33526
33650
|
url: "/gamestate",
|
|
33527
33651
|
body: { game: this.uuid } // !!! FIXME: Add delete_zombie parameter
|
|
@@ -33724,7 +33848,7 @@ class Game extends BaseGame {
|
|
|
33724
33848
|
this.chatLoading = true;
|
|
33725
33849
|
this.messages = [];
|
|
33726
33850
|
try {
|
|
33727
|
-
const result = await request({
|
|
33851
|
+
const result = await request(this.state, {
|
|
33728
33852
|
method: "POST",
|
|
33729
33853
|
url: "/chatload",
|
|
33730
33854
|
body: { channel: "game:" + this.uuid }
|
|
@@ -33750,7 +33874,7 @@ class Game extends BaseGame {
|
|
|
33750
33874
|
// Load statistics about a game
|
|
33751
33875
|
this.stats = undefined; // Error/in-progress status
|
|
33752
33876
|
try {
|
|
33753
|
-
const json = await request({
|
|
33877
|
+
const json = await request(this.state, {
|
|
33754
33878
|
method: "POST",
|
|
33755
33879
|
url: "/gamestats",
|
|
33756
33880
|
body: { game: this.uuid }
|
|
@@ -33770,7 +33894,7 @@ class Game extends BaseGame {
|
|
|
33770
33894
|
async sendMessage(msg) {
|
|
33771
33895
|
// Send a chat message
|
|
33772
33896
|
try {
|
|
33773
|
-
await request({
|
|
33897
|
+
await request(this.state, {
|
|
33774
33898
|
method: "POST",
|
|
33775
33899
|
url: "/chatmsg",
|
|
33776
33900
|
body: { channel: "game:" + this.uuid, msg: msg }
|
|
@@ -33972,10 +34096,10 @@ class Game extends BaseGame {
|
|
|
33972
34096
|
// Send a move to the server
|
|
33973
34097
|
this.moveInProgress = true;
|
|
33974
34098
|
try {
|
|
33975
|
-
const result = await request({
|
|
34099
|
+
const result = await request(this.state, {
|
|
33976
34100
|
method: "POST",
|
|
33977
34101
|
url: "/submitmove",
|
|
33978
|
-
body: { moves: moves, mcount: this.moves.length, uuid: this.uuid }
|
|
34102
|
+
body: { moves: moves, mcount: this.moves.length, uuid: this.uuid },
|
|
33979
34103
|
});
|
|
33980
34104
|
// The update() function also handles error results
|
|
33981
34105
|
this.update(result);
|
|
@@ -34001,7 +34125,7 @@ class Game extends BaseGame {
|
|
|
34001
34125
|
// Force resignation by a tardy opponent
|
|
34002
34126
|
this.moveInProgress = true;
|
|
34003
34127
|
try {
|
|
34004
|
-
const result = await request({
|
|
34128
|
+
const result = await request(this.state, {
|
|
34005
34129
|
method: "POST",
|
|
34006
34130
|
url: "/forceresign",
|
|
34007
34131
|
body: { mcount: this.moves.length, game: this.uuid }
|
|
@@ -34125,7 +34249,10 @@ const HOT_WARM_BOUNDARY_RATIO = 0.6;
|
|
|
34125
34249
|
const WARM_COLD_BOUNDARY_RATIO = 0.3;
|
|
34126
34250
|
class Riddle extends BaseGame {
|
|
34127
34251
|
constructor(uuid, date, model) {
|
|
34128
|
-
|
|
34252
|
+
if (!model.state) {
|
|
34253
|
+
throw new Error("No global state in Riddle constructor");
|
|
34254
|
+
}
|
|
34255
|
+
super(uuid, model.state);
|
|
34129
34256
|
// Scoring properties, static
|
|
34130
34257
|
this.bestPossibleScore = 0;
|
|
34131
34258
|
this.warmBoundary = 0;
|
|
@@ -34166,9 +34293,12 @@ class Riddle extends BaseGame {
|
|
|
34166
34293
|
async load(date, locale) {
|
|
34167
34294
|
this.date = date;
|
|
34168
34295
|
this.locale = locale;
|
|
34296
|
+
const { state } = this;
|
|
34169
34297
|
try {
|
|
34298
|
+
if (!state)
|
|
34299
|
+
throw new Error("No global state in Riddle.load");
|
|
34170
34300
|
// Request riddle data from server (HTTP API call)
|
|
34171
|
-
const response = await request({
|
|
34301
|
+
const response = await request(state, {
|
|
34172
34302
|
method: "POST",
|
|
34173
34303
|
url: "/gatadagsins/riddle",
|
|
34174
34304
|
body: { date, locale }
|
|
@@ -34196,11 +34326,11 @@ class Riddle extends BaseGame {
|
|
|
34196
34326
|
}
|
|
34197
34327
|
}
|
|
34198
34328
|
async submitRiddleWord(move) {
|
|
34199
|
-
const { state } = this
|
|
34329
|
+
const { state } = this;
|
|
34200
34330
|
if (!state || !state.userId)
|
|
34201
34331
|
return;
|
|
34202
34332
|
try {
|
|
34203
|
-
await request({
|
|
34333
|
+
await request(state, {
|
|
34204
34334
|
method: "POST",
|
|
34205
34335
|
url: "/gatadagsins/submit",
|
|
34206
34336
|
body: {
|
|
@@ -34480,8 +34610,6 @@ function getSettings() {
|
|
|
34480
34610
|
}
|
|
34481
34611
|
class Model {
|
|
34482
34612
|
constructor(settings, state) {
|
|
34483
|
-
// A class for the underlying data model, displayed by the current view
|
|
34484
|
-
this.state = null;
|
|
34485
34613
|
this.paths = [];
|
|
34486
34614
|
// The routeName will be "login", "main", "game"...
|
|
34487
34615
|
this.routeName = undefined;
|
|
@@ -34540,7 +34668,54 @@ class Model {
|
|
|
34540
34668
|
this.isExplo = state.isExplo;
|
|
34541
34669
|
this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
|
|
34542
34670
|
// Load localized text messages from the messages.json file
|
|
34543
|
-
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
|
+
});
|
|
34544
34719
|
}
|
|
34545
34720
|
async loadGame(uuid, funcComplete, deleteZombie = false) {
|
|
34546
34721
|
var _a;
|
|
@@ -34559,20 +34734,16 @@ class Model {
|
|
|
34559
34734
|
this.highlightedMove = null;
|
|
34560
34735
|
if (!uuid)
|
|
34561
34736
|
return; // Should not happen
|
|
34562
|
-
const result = await
|
|
34563
|
-
|
|
34564
|
-
|
|
34565
|
-
body: {
|
|
34566
|
-
game: uuid,
|
|
34567
|
-
delete_zombie: deleteZombie
|
|
34568
|
-
}
|
|
34737
|
+
const result = await this.post("/gamestate", {
|
|
34738
|
+
game: uuid,
|
|
34739
|
+
delete_zombie: deleteZombie
|
|
34569
34740
|
});
|
|
34570
34741
|
if (!(result === null || result === void 0 ? void 0 : result.ok)) {
|
|
34571
34742
|
// console.log("Game " + uuid + " could not be loaded");
|
|
34572
34743
|
}
|
|
34573
34744
|
else {
|
|
34574
34745
|
// Create a new game instance and load the state into it
|
|
34575
|
-
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);
|
|
34576
34747
|
// Successfully loaded: call the completion function, if given
|
|
34577
34748
|
// (this usually attaches the Firebase event listener)
|
|
34578
34749
|
funcComplete && funcComplete();
|
|
@@ -34595,11 +34766,7 @@ class Model {
|
|
|
34595
34766
|
this.numChallenges = 0;
|
|
34596
34767
|
this.oppReady = 0;
|
|
34597
34768
|
try {
|
|
34598
|
-
const json = await
|
|
34599
|
-
method: "POST",
|
|
34600
|
-
url: "/allgamelists",
|
|
34601
|
-
body: { zombie: includeZombies, count: 40 }
|
|
34602
|
-
});
|
|
34769
|
+
const json = await this.post("/allgamelists", { zombie: includeZombies, count: 40 });
|
|
34603
34770
|
if (!json || json.result !== 0) {
|
|
34604
34771
|
// An error occurred
|
|
34605
34772
|
this.gameList = [];
|
|
@@ -34641,11 +34808,7 @@ class Model {
|
|
|
34641
34808
|
this.numGames = 0;
|
|
34642
34809
|
this.spinners++;
|
|
34643
34810
|
try {
|
|
34644
|
-
const json = await
|
|
34645
|
-
method: "POST",
|
|
34646
|
-
url: "/gamelist",
|
|
34647
|
-
body: { zombie: includeZombies }
|
|
34648
|
-
});
|
|
34811
|
+
const json = await this.post("/gamelist", { zombie: includeZombies });
|
|
34649
34812
|
if (!json || json.result !== 0) {
|
|
34650
34813
|
// An error occurred
|
|
34651
34814
|
this.gameList = [];
|
|
@@ -34675,10 +34838,7 @@ class Model {
|
|
|
34675
34838
|
this.oppReady = 0;
|
|
34676
34839
|
this.spinners++; // Show spinner while loading
|
|
34677
34840
|
try {
|
|
34678
|
-
const json = await
|
|
34679
|
-
method: "POST",
|
|
34680
|
-
url: "/challengelist"
|
|
34681
|
-
});
|
|
34841
|
+
const json = await this.post("/challengelist");
|
|
34682
34842
|
if (!json || json.result !== 0) {
|
|
34683
34843
|
// An error occurred
|
|
34684
34844
|
this.challengeList = [];
|
|
@@ -34712,11 +34872,7 @@ class Model {
|
|
|
34712
34872
|
this.recentList = [];
|
|
34713
34873
|
this.spinners++; // Show spinner while loading
|
|
34714
34874
|
try {
|
|
34715
|
-
const json = await
|
|
34716
|
-
method: "POST",
|
|
34717
|
-
url: "/recentlist",
|
|
34718
|
-
body: { versus: null, count: 40 }
|
|
34719
|
-
});
|
|
34875
|
+
const json = await this.post("/recentlist", { versus: null, count: 40 });
|
|
34720
34876
|
if (!json || json.result !== 0) {
|
|
34721
34877
|
// An error occurred
|
|
34722
34878
|
this.recentList = [];
|
|
@@ -34735,11 +34891,7 @@ class Model {
|
|
|
34735
34891
|
}
|
|
34736
34892
|
async loadUserRecentList(userid, versus, readyFunc) {
|
|
34737
34893
|
// Load the list of recent games for the given user
|
|
34738
|
-
const json = await
|
|
34739
|
-
method: "POST",
|
|
34740
|
-
url: "/recentlist",
|
|
34741
|
-
body: { user: userid, versus: versus, count: 40 }
|
|
34742
|
-
});
|
|
34894
|
+
const json = await this.post("/recentlist", { user: userid, versus: versus, count: 40 });
|
|
34743
34895
|
readyFunc(json);
|
|
34744
34896
|
}
|
|
34745
34897
|
async loadUserList(criteria) {
|
|
@@ -34757,11 +34909,7 @@ class Model {
|
|
|
34757
34909
|
const url = "/userlist";
|
|
34758
34910
|
const body = criteria;
|
|
34759
34911
|
try {
|
|
34760
|
-
const json = await
|
|
34761
|
-
method: "POST",
|
|
34762
|
-
url,
|
|
34763
|
-
body,
|
|
34764
|
-
});
|
|
34912
|
+
const json = await this.post(url, body);
|
|
34765
34913
|
if (!json || json.result !== 0) {
|
|
34766
34914
|
// An error occurred
|
|
34767
34915
|
this.userList = [];
|
|
@@ -34783,11 +34931,7 @@ class Model {
|
|
|
34783
34931
|
const url = "/rating";
|
|
34784
34932
|
const body = { kind: spec };
|
|
34785
34933
|
try {
|
|
34786
|
-
const json = await
|
|
34787
|
-
method: "POST",
|
|
34788
|
-
url,
|
|
34789
|
-
body,
|
|
34790
|
-
});
|
|
34934
|
+
const json = await this.post(url, body);
|
|
34791
34935
|
if (!json || json.result !== 0) {
|
|
34792
34936
|
// An error occurred
|
|
34793
34937
|
this.eloRatingList = [];
|
|
@@ -34806,11 +34950,7 @@ class Model {
|
|
|
34806
34950
|
// Load statistics for the current user
|
|
34807
34951
|
this.ownStats = {};
|
|
34808
34952
|
try {
|
|
34809
|
-
const json = await
|
|
34810
|
-
method: "POST",
|
|
34811
|
-
url: "/userstats",
|
|
34812
|
-
body: {} // Current user is implicit
|
|
34813
|
-
});
|
|
34953
|
+
const json = await this.post("/userstats", {});
|
|
34814
34954
|
if (!json || json.result !== 0) {
|
|
34815
34955
|
// An error occurred
|
|
34816
34956
|
return;
|
|
@@ -34823,11 +34963,7 @@ class Model {
|
|
|
34823
34963
|
async loadUserStats(userid, readyFunc) {
|
|
34824
34964
|
// Load statistics for the given user
|
|
34825
34965
|
try {
|
|
34826
|
-
const json = await
|
|
34827
|
-
method: "POST",
|
|
34828
|
-
url: "/userstats",
|
|
34829
|
-
body: { user: userid }
|
|
34830
|
-
});
|
|
34966
|
+
const json = await this.post("/userstats", { user: userid });
|
|
34831
34967
|
readyFunc(json);
|
|
34832
34968
|
}
|
|
34833
34969
|
catch (e) {
|
|
@@ -34837,13 +34973,7 @@ class Model {
|
|
|
34837
34973
|
async loadPromoContent(key, readyFunc) {
|
|
34838
34974
|
// Load HTML content for promo dialog
|
|
34839
34975
|
try {
|
|
34840
|
-
const html = await
|
|
34841
|
-
method: "POST",
|
|
34842
|
-
url: "/promo",
|
|
34843
|
-
body: { key: key },
|
|
34844
|
-
responseType: "text",
|
|
34845
|
-
deserialize: (str) => str
|
|
34846
|
-
});
|
|
34976
|
+
const html = await this.postText("/promo", { key: key });
|
|
34847
34977
|
readyFunc(html);
|
|
34848
34978
|
}
|
|
34849
34979
|
catch (e) {
|
|
@@ -34892,11 +35022,7 @@ class Model {
|
|
|
34892
35022
|
rack,
|
|
34893
35023
|
limit: NUM_BEST_MOVES,
|
|
34894
35024
|
};
|
|
34895
|
-
const json = await
|
|
34896
|
-
method: "POST",
|
|
34897
|
-
url: "/moves",
|
|
34898
|
-
body: rq,
|
|
34899
|
-
});
|
|
35025
|
+
const json = await this.postMoves(rq);
|
|
34900
35026
|
this.highlightedMove = null;
|
|
34901
35027
|
if (!json || json.moves === undefined) {
|
|
34902
35028
|
// Something unexpected going on
|
|
@@ -34931,12 +35057,7 @@ class Model {
|
|
|
34931
35057
|
return; // Already loaded
|
|
34932
35058
|
try {
|
|
34933
35059
|
const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
34934
|
-
const result = await
|
|
34935
|
-
method: "GET",
|
|
34936
|
-
url: "/rawhelp?version=malstadur&locale=" + locale,
|
|
34937
|
-
responseType: "text",
|
|
34938
|
-
deserialize: (str) => str
|
|
34939
|
-
});
|
|
35060
|
+
const result = await this.getText("/rawhelp?version=malstadur&locale=" + locale);
|
|
34940
35061
|
this.helpHTML = result;
|
|
34941
35062
|
}
|
|
34942
35063
|
catch (e) {
|
|
@@ -34951,12 +35072,7 @@ class Model {
|
|
|
34951
35072
|
return; // Already loaded
|
|
34952
35073
|
try {
|
|
34953
35074
|
const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
34954
|
-
const result = await
|
|
34955
|
-
method: "GET",
|
|
34956
|
-
url: "/friend?locale=" + locale,
|
|
34957
|
-
responseType: "text",
|
|
34958
|
-
deserialize: (str) => str
|
|
34959
|
-
});
|
|
35075
|
+
const result = await this.getText("/friend?locale=" + locale);
|
|
34960
35076
|
this.friendHTML = result;
|
|
34961
35077
|
}
|
|
34962
35078
|
catch (e) {
|
|
@@ -34972,10 +35088,7 @@ class Model {
|
|
|
34972
35088
|
this.spinners++;
|
|
34973
35089
|
}
|
|
34974
35090
|
try {
|
|
34975
|
-
const result = await
|
|
34976
|
-
method: "POST",
|
|
34977
|
-
url: "/loaduserprefs",
|
|
34978
|
-
});
|
|
35091
|
+
const result = await this.post("/loaduserprefs");
|
|
34979
35092
|
if (!result || !result.ok) {
|
|
34980
35093
|
this.user = null;
|
|
34981
35094
|
this.userErrors = null;
|
|
@@ -35002,11 +35115,7 @@ class Model {
|
|
|
35002
35115
|
if (!user)
|
|
35003
35116
|
return;
|
|
35004
35117
|
try {
|
|
35005
|
-
const result = await
|
|
35006
|
-
method: "POST",
|
|
35007
|
-
url: "/saveuserprefs",
|
|
35008
|
-
body: user
|
|
35009
|
-
});
|
|
35118
|
+
const result = await this.post("/saveuserprefs", user);
|
|
35010
35119
|
if (result === null || result === void 0 ? void 0 : result.ok) {
|
|
35011
35120
|
// User preferences modified successfully on the server:
|
|
35012
35121
|
// update the state variables that we're caching
|
|
@@ -35420,8 +35529,10 @@ class Actions {
|
|
|
35420
35529
|
}
|
|
35421
35530
|
async markFavorite(userId, status) {
|
|
35422
35531
|
// Mark or de-mark a user as a favorite
|
|
35532
|
+
if (!this.model.state)
|
|
35533
|
+
return;
|
|
35423
35534
|
try {
|
|
35424
|
-
await request({
|
|
35535
|
+
await request(this.model.state, {
|
|
35425
35536
|
method: "POST",
|
|
35426
35537
|
url: "/favorite",
|
|
35427
35538
|
body: { destuser: userId, action: status ? "add" : "delete" }
|
|
@@ -35439,8 +35550,10 @@ class Actions {
|
|
|
35439
35550
|
async handleChallenge(parameters) {
|
|
35440
35551
|
var _a;
|
|
35441
35552
|
// Reject or retract a challenge
|
|
35553
|
+
if (!this.model.state)
|
|
35554
|
+
return;
|
|
35442
35555
|
try {
|
|
35443
|
-
const json = await request({
|
|
35556
|
+
const json = await request(this.model.state, {
|
|
35444
35557
|
method: "POST",
|
|
35445
35558
|
url: "/challenge",
|
|
35446
35559
|
body: parameters
|
|
@@ -35495,6 +35608,8 @@ class Actions {
|
|
|
35495
35608
|
async startNewGame(oppid, reverse = false) {
|
|
35496
35609
|
var _a;
|
|
35497
35610
|
// Ask the server to initiate a new game against the given opponent
|
|
35611
|
+
if (!this.model.state)
|
|
35612
|
+
return;
|
|
35498
35613
|
try {
|
|
35499
35614
|
const rqBody = { opp: oppid, rev: reverse };
|
|
35500
35615
|
if (this.model.isExplo) {
|
|
@@ -35507,7 +35622,7 @@ class Actions {
|
|
|
35507
35622
|
url: "/initgame",
|
|
35508
35623
|
body: rqBody
|
|
35509
35624
|
};
|
|
35510
|
-
const json = await request(rq);
|
|
35625
|
+
const json = await request(this.model.state, rq);
|
|
35511
35626
|
if (json === null || json === void 0 ? void 0 : json.ok) {
|
|
35512
35627
|
// Log the new game event
|
|
35513
35628
|
const locale = ((_a = this.model.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
@@ -35573,8 +35688,10 @@ class Actions {
|
|
|
35573
35688
|
// User Preference Management Actions
|
|
35574
35689
|
async setUserPref(pref) {
|
|
35575
35690
|
// Set a user preference on the server
|
|
35691
|
+
if (!this.model.state)
|
|
35692
|
+
return;
|
|
35576
35693
|
try {
|
|
35577
|
-
await request({
|
|
35694
|
+
await request(this.model.state, {
|
|
35578
35695
|
method: "POST",
|
|
35579
35696
|
url: "/setuserpref",
|
|
35580
35697
|
body: pref
|
|
@@ -35591,7 +35708,7 @@ class Actions {
|
|
|
35591
35708
|
if (!user || !state)
|
|
35592
35709
|
return false;
|
|
35593
35710
|
try {
|
|
35594
|
-
const json = await request({
|
|
35711
|
+
const json = await request(state, {
|
|
35595
35712
|
method: "POST",
|
|
35596
35713
|
url: "/cancelplan",
|
|
35597
35714
|
body: {}
|
|
@@ -35709,41 +35826,25 @@ async function main$1(state, container) {
|
|
|
35709
35826
|
console.error("No container element found");
|
|
35710
35827
|
return "error";
|
|
35711
35828
|
}
|
|
35712
|
-
// Set up Netskrafl backend server URLs
|
|
35713
|
-
setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
|
|
35714
35829
|
try {
|
|
35715
|
-
|
|
35716
|
-
|
|
35717
|
-
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35721
|
-
|
|
35722
|
-
|
|
35723
|
-
|
|
35724
|
-
state.userNick = loginData.nickname || state.userNick;
|
|
35725
|
-
// Log in to Firebase with the token passed from the server
|
|
35726
|
-
await loginFirebase(state, loginData.firebase_token);
|
|
35727
|
-
// Everything looks OK:
|
|
35728
|
-
// Create the model, actions and view objects in proper sequence
|
|
35729
|
-
const settings = getSettings();
|
|
35730
|
-
const model = new Model(settings, state);
|
|
35731
|
-
const actions = new Actions(model);
|
|
35732
|
-
const view = new View(actions);
|
|
35733
|
-
// Run the Mithril router
|
|
35734
|
-
const routeResolver = createRouteResolver(actions, view);
|
|
35735
|
-
m.route(container, settings.defaultRoute, routeResolver);
|
|
35736
|
-
return "success";
|
|
35737
|
-
}
|
|
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);
|
|
35738
35839
|
}
|
|
35739
35840
|
catch (e) {
|
|
35740
|
-
console.error("Exception during
|
|
35841
|
+
console.error("Exception during initialization: ", e);
|
|
35842
|
+
return "error";
|
|
35741
35843
|
}
|
|
35742
|
-
|
|
35743
|
-
return "error";
|
|
35844
|
+
return "success";
|
|
35744
35845
|
}
|
|
35745
35846
|
|
|
35746
|
-
const mountForUser$1 = async (state
|
|
35847
|
+
const mountForUser$1 = async (state) => {
|
|
35747
35848
|
// Return a DOM tree containing a mounted Netskrafl UI
|
|
35748
35849
|
// for the user specified in the state object
|
|
35749
35850
|
const { userEmail } = state;
|
|
@@ -35771,18 +35872,12 @@ const mountForUser$1 = async (state, tokenExpired) => {
|
|
|
35771
35872
|
root.className = "netskrafl-user";
|
|
35772
35873
|
return root;
|
|
35773
35874
|
}
|
|
35774
|
-
else if (loginResult === "expired") {
|
|
35775
|
-
// We need a new token from the Málstaður backend
|
|
35776
|
-
root.className = "netskrafl-expired";
|
|
35777
|
-
tokenExpired && tokenExpired(); // This causes a reload of the component
|
|
35778
|
-
return root;
|
|
35779
|
-
}
|
|
35780
35875
|
// console.error("Failed to mount Netskrafl UI for user", userEmail);
|
|
35781
35876
|
throw new Error("Failed to mount Netskrafl UI");
|
|
35782
35877
|
};
|
|
35783
35878
|
const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
35784
35879
|
const ref = React.createRef();
|
|
35785
|
-
const completeState = { ...
|
|
35880
|
+
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
35786
35881
|
const { userEmail } = completeState;
|
|
35787
35882
|
/*
|
|
35788
35883
|
useEffect(() => {
|
|
@@ -35815,7 +35910,7 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
|
35815
35910
|
return;
|
|
35816
35911
|
}
|
|
35817
35912
|
try {
|
|
35818
|
-
mountForUser$1(completeState
|
|
35913
|
+
mountForUser$1(completeState).then((div) => {
|
|
35819
35914
|
// Attach the div as a child of the container
|
|
35820
35915
|
// instead of any previous children
|
|
35821
35916
|
const container = ref.current;
|
|
@@ -36375,44 +36470,28 @@ async function main(state, container) {
|
|
|
36375
36470
|
console.error("No container element found");
|
|
36376
36471
|
return "error";
|
|
36377
36472
|
}
|
|
36378
|
-
// Set up Netskrafl backend server URLs
|
|
36379
|
-
setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
|
|
36380
36473
|
try {
|
|
36381
|
-
|
|
36382
|
-
|
|
36383
|
-
|
|
36384
|
-
|
|
36385
|
-
|
|
36386
|
-
|
|
36387
|
-
|
|
36388
|
-
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
36392
|
-
|
|
36393
|
-
// Everything looks OK:
|
|
36394
|
-
// Create the model, view and actions objects
|
|
36395
|
-
const settings = getSettings();
|
|
36396
|
-
const model = new Model(settings, state);
|
|
36397
|
-
const actions = new Actions(model);
|
|
36398
|
-
const view = new View(actions);
|
|
36399
|
-
const today = new Date().toISOString().split("T")[0];
|
|
36400
|
-
const locale = state.locale || "is_IS";
|
|
36401
|
-
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36402
|
-
m.mount(container, {
|
|
36403
|
-
view: () => m(GataDagsins$1, { view, date: today, locale }),
|
|
36404
|
-
});
|
|
36405
|
-
return "success";
|
|
36406
|
-
}
|
|
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
|
+
});
|
|
36407
36486
|
}
|
|
36408
36487
|
catch (e) {
|
|
36409
|
-
console.error("Exception during
|
|
36488
|
+
console.error("Exception during initialization: ", e);
|
|
36489
|
+
return "error";
|
|
36410
36490
|
}
|
|
36411
|
-
|
|
36412
|
-
return "error";
|
|
36491
|
+
return "success";
|
|
36413
36492
|
}
|
|
36414
36493
|
|
|
36415
|
-
const mountForUser = async (state
|
|
36494
|
+
const mountForUser = async (state) => {
|
|
36416
36495
|
// Return a DOM tree containing a mounted Gáta Dagsins UI
|
|
36417
36496
|
// for the user specified in the state object
|
|
36418
36497
|
const { userEmail } = state;
|
|
@@ -36440,18 +36519,12 @@ const mountForUser = async (state, tokenExpired) => {
|
|
|
36440
36519
|
root.className = "gatadagsins-user";
|
|
36441
36520
|
return root;
|
|
36442
36521
|
}
|
|
36443
|
-
else if (loginResult === "expired") {
|
|
36444
|
-
// We need a new token from the Málstaður backend
|
|
36445
|
-
root.className = "gatadagsins-expired";
|
|
36446
|
-
tokenExpired && tokenExpired(); // This causes a reload of the component
|
|
36447
|
-
return root;
|
|
36448
|
-
}
|
|
36449
36522
|
// console.error("Failed to mount Gáta Dagsins UI for user", userEmail);
|
|
36450
36523
|
throw new Error("Failed to mount Gáta Dagsins UI");
|
|
36451
36524
|
};
|
|
36452
36525
|
const GataDagsinsImpl = ({ state, tokenExpired }) => {
|
|
36453
36526
|
const ref = React.createRef();
|
|
36454
|
-
const completeState = { ...
|
|
36527
|
+
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
36455
36528
|
const { userEmail } = completeState;
|
|
36456
36529
|
useEffect(() => {
|
|
36457
36530
|
var _a;
|
|
@@ -36469,7 +36542,7 @@ const GataDagsinsImpl = ({ state, tokenExpired }) => {
|
|
|
36469
36542
|
return;
|
|
36470
36543
|
}
|
|
36471
36544
|
try {
|
|
36472
|
-
mountForUser(completeState
|
|
36545
|
+
mountForUser(completeState).then((div) => {
|
|
36473
36546
|
// Attach the div as a child of the container
|
|
36474
36547
|
// instead of any previous children
|
|
36475
36548
|
const container = ref.current;
|