@mideind/netskrafl-react 1.0.0-beta.9 → 1.0.2
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 +274 -10
- package/dist/cjs/index.js +1171 -893
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +274 -10
- package/dist/esm/index.js +1171 -893
- package/dist/esm/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -3,6 +3,98 @@
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var React = require('react');
|
|
5
5
|
|
|
6
|
+
// Key for storing auth settings in sessionStorage
|
|
7
|
+
const AUTH_SETTINGS_KEY = "netskrafl_auth_settings";
|
|
8
|
+
// Save authentication settings to sessionStorage
|
|
9
|
+
const saveAuthSettings = (settings) => {
|
|
10
|
+
if (!settings) {
|
|
11
|
+
clearAuthSettings();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
// Filter to only include properties defined in PersistedAuthSettings interface
|
|
16
|
+
const filteredSettings = {
|
|
17
|
+
userEmail: settings.userEmail, // Required field
|
|
18
|
+
};
|
|
19
|
+
// Only add optional fields if they are defined
|
|
20
|
+
if (settings.userId !== undefined)
|
|
21
|
+
filteredSettings.userId = settings.userId;
|
|
22
|
+
if (settings.userNick !== undefined)
|
|
23
|
+
filteredSettings.userNick = settings.userNick;
|
|
24
|
+
if (settings.firebaseAPIKey !== undefined)
|
|
25
|
+
filteredSettings.firebaseAPIKey = settings.firebaseAPIKey;
|
|
26
|
+
if (settings.beginner !== undefined)
|
|
27
|
+
filteredSettings.beginner = settings.beginner;
|
|
28
|
+
if (settings.fairPlay !== undefined)
|
|
29
|
+
filteredSettings.fairPlay = settings.fairPlay;
|
|
30
|
+
if (settings.ready !== undefined)
|
|
31
|
+
filteredSettings.ready = settings.ready;
|
|
32
|
+
if (settings.readyTimed !== undefined)
|
|
33
|
+
filteredSettings.readyTimed = settings.readyTimed;
|
|
34
|
+
// Only save if we have actual settings to persist
|
|
35
|
+
if (Object.keys(filteredSettings).length > 1) {
|
|
36
|
+
sessionStorage.setItem(AUTH_SETTINGS_KEY, JSON.stringify(filteredSettings));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
clearAuthSettings();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// SessionStorage might be unavailable or full
|
|
44
|
+
console.warn("Could not save auth settings to sessionStorage:", error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
// Retrieve authentication settings from sessionStorage
|
|
48
|
+
const loadAuthSettings = () => {
|
|
49
|
+
try {
|
|
50
|
+
const stored = sessionStorage.getItem(AUTH_SETTINGS_KEY);
|
|
51
|
+
if (stored) {
|
|
52
|
+
return JSON.parse(stored);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// SessionStorage might be unavailable or data might be corrupted
|
|
57
|
+
console.warn("Could not load auth settings from sessionStorage:", error);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
// Clear authentication settings from sessionStorage
|
|
62
|
+
const clearAuthSettings = () => {
|
|
63
|
+
try {
|
|
64
|
+
sessionStorage.removeItem(AUTH_SETTINGS_KEY);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn("Could not clear auth settings from sessionStorage:", error);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
// Apply persisted settings to a GlobalState object
|
|
71
|
+
const applyPersistedSettings = (state) => {
|
|
72
|
+
var _a, _b, _c, _d;
|
|
73
|
+
const persisted = loadAuthSettings();
|
|
74
|
+
if (!persisted) {
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
// CRITICAL SECURITY CHECK: Only apply persisted settings if they belong to the current user
|
|
78
|
+
// This prevents data leakage between different users in the same browser session
|
|
79
|
+
if (persisted.userEmail !== state.userEmail) {
|
|
80
|
+
// Different user detected - clear the old user's settings
|
|
81
|
+
clearAuthSettings();
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
// Apply persisted settings, but don't override values explicitly passed in props
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
// Only apply persisted values if current values are defaults
|
|
88
|
+
userId: state.userId || persisted.userId || state.userId,
|
|
89
|
+
userNick: state.userNick || persisted.userNick || state.userNick,
|
|
90
|
+
firebaseAPIKey: state.firebaseAPIKey || persisted.firebaseAPIKey || state.firebaseAPIKey,
|
|
91
|
+
beginner: (_a = persisted.beginner) !== null && _a !== void 0 ? _a : state.beginner,
|
|
92
|
+
fairPlay: (_b = persisted.fairPlay) !== null && _b !== void 0 ? _b : state.fairPlay,
|
|
93
|
+
ready: (_c = persisted.ready) !== null && _c !== void 0 ? _c : state.ready,
|
|
94
|
+
readyTimed: (_d = persisted.readyTimed) !== null && _d !== void 0 ? _d : state.readyTimed,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
6
98
|
const DEFAULT_STATE = {
|
|
7
99
|
projectId: "netskrafl",
|
|
8
100
|
firebaseAPIKey: "",
|
|
@@ -23,16 +115,39 @@ const DEFAULT_STATE = {
|
|
|
23
115
|
loginUrl: "",
|
|
24
116
|
loginMethod: "",
|
|
25
117
|
newUser: false,
|
|
26
|
-
beginner:
|
|
27
|
-
fairPlay:
|
|
118
|
+
beginner: true,
|
|
119
|
+
fairPlay: false,
|
|
28
120
|
plan: "", // Not a friend
|
|
29
121
|
hasPaid: false,
|
|
30
|
-
ready:
|
|
31
|
-
readyTimed:
|
|
122
|
+
ready: true,
|
|
123
|
+
readyTimed: true,
|
|
32
124
|
uiFullscreen: true,
|
|
33
125
|
uiLandscape: false,
|
|
34
126
|
runningLocal: false,
|
|
35
127
|
};
|
|
128
|
+
const makeServerUrls = (backendUrl, movesUrl) => {
|
|
129
|
+
// If the last character of the url is a slash, cut it off,
|
|
130
|
+
// since path URLs always start with a slash
|
|
131
|
+
const cleanupUrl = (url) => {
|
|
132
|
+
if (url.length > 0 && url[url.length - 1] === "/") {
|
|
133
|
+
url = url.slice(0, -1);
|
|
134
|
+
}
|
|
135
|
+
return url;
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
serverUrl: cleanupUrl(backendUrl),
|
|
139
|
+
movesUrl: cleanupUrl(movesUrl),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const makeGlobalState = (overrides) => {
|
|
143
|
+
const state = {
|
|
144
|
+
...DEFAULT_STATE,
|
|
145
|
+
...overrides,
|
|
146
|
+
};
|
|
147
|
+
const stateWithUrls = { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
|
|
148
|
+
// Apply any persisted authentication settings from sessionStorage
|
|
149
|
+
return applyPersistedSettings(stateWithUrls);
|
|
150
|
+
};
|
|
36
151
|
|
|
37
152
|
function getDefaultExportFromCjs (x) {
|
|
38
153
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -2189,53 +2304,485 @@ function requireMithril () {
|
|
|
2189
2304
|
var mithrilExports = requireMithril();
|
|
2190
2305
|
var m = /*@__PURE__*/getDefaultExportFromCjs(mithrilExports);
|
|
2191
2306
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
let MOVES_ACCESS_KEY = "None";
|
|
2195
|
-
const setServerUrl = (backendUrl, movesUrl, movesAccessKey) => {
|
|
2196
|
-
// If the last character of the url is a slash, cut it off,
|
|
2197
|
-
// since path URLs always start with a slash
|
|
2198
|
-
const cleanupUrl = (url) => {
|
|
2199
|
-
if (url.length > 0 && url[url.length - 1] === "/") {
|
|
2200
|
-
url = url.slice(0, -1);
|
|
2201
|
-
}
|
|
2202
|
-
return url;
|
|
2203
|
-
};
|
|
2204
|
-
if (backendUrl)
|
|
2205
|
-
BACKEND_SERVER_PREFIX = cleanupUrl(backendUrl);
|
|
2206
|
-
if (movesUrl)
|
|
2207
|
-
MOVES_SERVER_PREFIX = cleanupUrl(movesUrl);
|
|
2208
|
-
if (movesAccessKey)
|
|
2209
|
-
MOVES_ACCESS_KEY = movesAccessKey;
|
|
2210
|
-
};
|
|
2211
|
-
const serverUrl = (path) => {
|
|
2212
|
-
return `${BACKEND_SERVER_PREFIX}${path}`;
|
|
2307
|
+
const serverUrl = (state, path) => {
|
|
2308
|
+
return `${state.serverUrl}${path}`;
|
|
2213
2309
|
};
|
|
2214
|
-
|
|
2215
|
-
|
|
2310
|
+
// Export a non-authenticated request for use by login.ts to avoid circular dependency
|
|
2311
|
+
const requestWithoutAuth = (state, options) => {
|
|
2216
2312
|
return m.request({
|
|
2217
2313
|
withCredentials: true,
|
|
2218
2314
|
...options,
|
|
2219
|
-
url: serverUrl(options.url),
|
|
2315
|
+
url: serverUrl(state, options.url),
|
|
2220
2316
|
});
|
|
2221
2317
|
};
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2318
|
+
|
|
2319
|
+
/*
|
|
2320
|
+
|
|
2321
|
+
i8n.ts
|
|
2322
|
+
|
|
2323
|
+
Single page UI for Netskrafl/Explo using the Mithril library
|
|
2324
|
+
|
|
2325
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2326
|
+
Author: Vilhjálmur Þorsteinsson
|
|
2327
|
+
|
|
2328
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2329
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2330
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2331
|
+
|
|
2332
|
+
|
|
2333
|
+
This module contains internationalization (i18n) utility functions,
|
|
2334
|
+
allowing for translation of displayed text between languages.
|
|
2335
|
+
|
|
2336
|
+
Text messages for individual locales are loaded from the
|
|
2337
|
+
/static/assets/messages.json file, which is fetched from the server.
|
|
2338
|
+
|
|
2339
|
+
*/
|
|
2340
|
+
// Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
|
|
2341
|
+
// This is overwritten in setLocale()
|
|
2342
|
+
let currentLocale = "is_IS";
|
|
2343
|
+
let currentFallback = "is";
|
|
2344
|
+
// Regex that matches embedded interpolations such as "Welcome, {username}!"
|
|
2345
|
+
// Interpolation identifiers should only contain ASCII characters, digits and '_'
|
|
2346
|
+
const rex = /{\s*(\w+)\s*}/g;
|
|
2347
|
+
let messages = {};
|
|
2348
|
+
let messagesLoaded = false;
|
|
2349
|
+
function hasAnyTranslation(msgs, locale) {
|
|
2350
|
+
// Return true if any translation is available for the given locale
|
|
2351
|
+
for (let key in msgs) {
|
|
2352
|
+
if (msgs[key][locale] !== undefined)
|
|
2353
|
+
return true;
|
|
2354
|
+
}
|
|
2355
|
+
return false;
|
|
2356
|
+
}
|
|
2357
|
+
function setLocale(locale, msgs) {
|
|
2358
|
+
// Set the current i18n locale and fallback
|
|
2359
|
+
currentLocale = locale;
|
|
2360
|
+
currentFallback = locale.split("_")[0];
|
|
2361
|
+
// For unsupported locales, i.e. locales that have no
|
|
2362
|
+
// translations available for them, fall back to English (U.S.).
|
|
2363
|
+
if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
|
|
2364
|
+
currentLocale = "en_US";
|
|
2365
|
+
currentFallback = "en";
|
|
2366
|
+
}
|
|
2367
|
+
// Flatten the Messages structure, enabling long strings
|
|
2368
|
+
// to be represented as string arrays in the messages.json file
|
|
2369
|
+
messages = {};
|
|
2370
|
+
for (let key in msgs) {
|
|
2371
|
+
for (let lc in msgs[key]) {
|
|
2372
|
+
let s = msgs[key][lc];
|
|
2373
|
+
if (Array.isArray(s))
|
|
2374
|
+
s = s.join("");
|
|
2375
|
+
if (messages[key] === undefined)
|
|
2376
|
+
messages[key] = {};
|
|
2377
|
+
// If the string s contains HTML markup of the form <tag>...</tag>,
|
|
2378
|
+
// convert it into a list of Mithril Vnode children corresponding to
|
|
2379
|
+
// the text and the tags
|
|
2380
|
+
if (s.match(/<[a-z]+>/)) {
|
|
2381
|
+
// Looks like the string contains HTML markup
|
|
2382
|
+
const vnodes = [];
|
|
2383
|
+
let i = 0;
|
|
2384
|
+
let tagMatch = null;
|
|
2385
|
+
while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
|
|
2386
|
+
// Found what looks like an HTML tag
|
|
2387
|
+
// Calculate the index of the enclosed text within s
|
|
2388
|
+
const tag = tagMatch[0];
|
|
2389
|
+
let j = i + tagMatch.index + tag.length;
|
|
2390
|
+
// Find the end tag
|
|
2391
|
+
let end = s.indexOf("</" + tag.slice(1), j);
|
|
2392
|
+
if (end < 0) {
|
|
2393
|
+
// No end tag - skip past this weirdness
|
|
2394
|
+
i = j;
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
// Add the text preceding the tag
|
|
2398
|
+
if (tagMatch.index > 0)
|
|
2399
|
+
vnodes.push(s.slice(i, i + tagMatch.index));
|
|
2400
|
+
// Create the Mithril node corresponding to the tag and the enclosed text
|
|
2401
|
+
// and add it to the list
|
|
2402
|
+
vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
|
|
2403
|
+
// Advance the index past the end of the tag
|
|
2404
|
+
i = end + tag.length + 1;
|
|
2405
|
+
}
|
|
2406
|
+
// Push the final text part, if any
|
|
2407
|
+
if (i < s.length)
|
|
2408
|
+
vnodes.push(s.slice(i));
|
|
2409
|
+
// Reassign s to the list of vnodes
|
|
2410
|
+
s = vnodes;
|
|
2411
|
+
}
|
|
2412
|
+
messages[key][lc] = s;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
messagesLoaded = true;
|
|
2416
|
+
}
|
|
2417
|
+
async function loadMessages(state, locale) {
|
|
2418
|
+
// Load the internationalization message JSON file from the server
|
|
2419
|
+
// and set the user's locale
|
|
2420
|
+
try {
|
|
2421
|
+
const messages = await requestWithoutAuth(state, {
|
|
2422
|
+
method: "GET",
|
|
2423
|
+
url: "/static/assets/messages.json",
|
|
2424
|
+
withCredentials: false, // Cookies are not allowed for CORS request
|
|
2425
|
+
});
|
|
2426
|
+
setLocale(locale, messages);
|
|
2427
|
+
}
|
|
2428
|
+
catch (_a) {
|
|
2429
|
+
setLocale(locale, {});
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
function t(key, ips = {}) {
|
|
2433
|
+
// Main text translation function, supporting interpolation
|
|
2434
|
+
// and HTML tag substitution
|
|
2435
|
+
const msgDict = messages[key];
|
|
2436
|
+
if (msgDict === undefined)
|
|
2437
|
+
// No dictionary for this key - may actually be a missing entry
|
|
2438
|
+
return messagesLoaded ? key : "";
|
|
2439
|
+
// Lookup exact locale, then fallback, then resort to returning the key
|
|
2440
|
+
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
2441
|
+
// If we have an interpolation object, do the interpolation first
|
|
2442
|
+
return Object.keys(ips).length ? interpolate(message, ips) : message;
|
|
2443
|
+
}
|
|
2444
|
+
function ts(key, ips = {}) {
|
|
2445
|
+
// String translation function, supporting interpolation
|
|
2446
|
+
// but not HTML tag substitution
|
|
2447
|
+
const msgDict = messages[key];
|
|
2448
|
+
if (msgDict === undefined)
|
|
2449
|
+
// No dictionary for this key - may actually be a missing entry
|
|
2450
|
+
return messagesLoaded ? key : "";
|
|
2451
|
+
// Lookup exact locale, then fallback, then resort to returning the key
|
|
2452
|
+
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
2453
|
+
if (typeof message != "string")
|
|
2454
|
+
// This is actually an error - the client should be calling t() instead
|
|
2455
|
+
return "";
|
|
2456
|
+
// If we have an interpolation object, do the interpolation first
|
|
2457
|
+
return Object.keys(ips).length ? interpolate_string(message, ips) : message;
|
|
2458
|
+
}
|
|
2459
|
+
function mt(cls, children) {
|
|
2460
|
+
// Wrapper for the Mithril m() function that auto-translates
|
|
2461
|
+
// string and array arguments
|
|
2462
|
+
if (typeof children == "string") {
|
|
2463
|
+
return m(cls, t(children));
|
|
2464
|
+
}
|
|
2465
|
+
if (Array.isArray(children)) {
|
|
2466
|
+
return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
|
|
2467
|
+
}
|
|
2468
|
+
return m(cls, children);
|
|
2469
|
+
}
|
|
2470
|
+
function interpolate(message, ips) {
|
|
2471
|
+
// Replace interpolation placeholders with their corresponding values
|
|
2472
|
+
if (typeof message == "string") {
|
|
2473
|
+
return message.replace(rex, (match, key) => ips[key] || match);
|
|
2474
|
+
}
|
|
2475
|
+
if (Array.isArray(message)) {
|
|
2476
|
+
return message.map((item) => interpolate(item, ips));
|
|
2477
|
+
}
|
|
2478
|
+
return message;
|
|
2479
|
+
}
|
|
2480
|
+
function interpolate_string(message, ips) {
|
|
2481
|
+
// Replace interpolation placeholders with their corresponding values
|
|
2482
|
+
return message.replace(rex, (match, key) => ips[key] || match);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/*
|
|
2486
|
+
|
|
2487
|
+
Types.ts
|
|
2488
|
+
|
|
2489
|
+
Common type definitions for the Explo/Netskrafl user interface
|
|
2490
|
+
|
|
2491
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2492
|
+
Author: Vilhjalmur Thorsteinsson
|
|
2493
|
+
|
|
2494
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2495
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2496
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2497
|
+
|
|
2498
|
+
*/
|
|
2499
|
+
// Global constants
|
|
2500
|
+
const RACK_SIZE = 7;
|
|
2501
|
+
const ROWIDS = "ABCDEFGHIJKLMNO";
|
|
2502
|
+
const BOARD_SIZE = ROWIDS.length;
|
|
2503
|
+
const EXTRA_WIDE_LETTERS = "q";
|
|
2504
|
+
const WIDE_LETTERS = "zxmæ";
|
|
2505
|
+
const ZOOM_FACTOR = 1.5;
|
|
2506
|
+
const ERROR_MESSAGES = {
|
|
2507
|
+
// Translations are found in /static/assets/messages.json
|
|
2508
|
+
1: "Enginn stafur lagður niður",
|
|
2509
|
+
2: "Fyrsta orð verður að liggja um byrjunarreitinn",
|
|
2510
|
+
3: "Orð verður að vera samfellt á borðinu",
|
|
2511
|
+
4: "Orð verður að tengjast orði sem fyrir er",
|
|
2512
|
+
5: "Reitur þegar upptekinn",
|
|
2513
|
+
6: "Ekki má vera eyða í orði",
|
|
2514
|
+
7: "word_not_found",
|
|
2515
|
+
8: "word_not_found",
|
|
2516
|
+
9: "Of margir stafir lagðir niður",
|
|
2517
|
+
10: "Stafur er ekki í rekkanum",
|
|
2518
|
+
11: "Of fáir stafir eftir, skipting ekki leyfð",
|
|
2519
|
+
12: "Of mörgum stöfum skipt",
|
|
2520
|
+
13: "Leik vantar á borðið - notið F5/Refresh",
|
|
2521
|
+
14: "Notandi ekki innskráður - notið F5/Refresh",
|
|
2522
|
+
15: "Rangur eða óþekktur notandi",
|
|
2523
|
+
16: "Viðureign finnst ekki",
|
|
2524
|
+
17: "Viðureign er ekki utan tímamarka",
|
|
2525
|
+
18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
|
|
2526
|
+
19: "Véfenging er ekki möguleg í þessari viðureign",
|
|
2527
|
+
20: "Síðasti leikur er ekki véfengjanlegur",
|
|
2528
|
+
21: "Aðeins véfenging eða pass leyfileg",
|
|
2529
|
+
"server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
|
|
2237
2530
|
};
|
|
2238
2531
|
|
|
2532
|
+
/*
|
|
2533
|
+
|
|
2534
|
+
Util.ts
|
|
2535
|
+
|
|
2536
|
+
Utility functions for the Explo/Netskrafl user interface
|
|
2537
|
+
|
|
2538
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2539
|
+
Author: Vilhjálmur Þorsteinsson
|
|
2540
|
+
|
|
2541
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2542
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2543
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2544
|
+
|
|
2545
|
+
The following code is based on
|
|
2546
|
+
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
|
|
2547
|
+
|
|
2548
|
+
*/
|
|
2549
|
+
// Global vars to cache event state
|
|
2550
|
+
var evCache = [];
|
|
2551
|
+
var origDistance = -1;
|
|
2552
|
+
const PINCH_THRESHOLD = 10; // Minimum pinch movement
|
|
2553
|
+
var hasZoomed = false;
|
|
2554
|
+
// Old-style (non-single-page) game URL prefix
|
|
2555
|
+
const BOARD_PREFIX = "/board?game=";
|
|
2556
|
+
const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
|
|
2557
|
+
function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
|
|
2558
|
+
// Install event handlers for the pointer target
|
|
2559
|
+
attrs.onpointerdown = pointerdown_handler;
|
|
2560
|
+
attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
|
|
2561
|
+
// Use same handler for pointer{up,cancel,out,leave} events since
|
|
2562
|
+
// the semantics for these events - in this app - are the same.
|
|
2563
|
+
attrs.onpointerup = pointerup_handler;
|
|
2564
|
+
attrs.onpointercancel = pointerup_handler;
|
|
2565
|
+
attrs.onpointerout = pointerup_handler;
|
|
2566
|
+
attrs.onpointerleave = pointerup_handler;
|
|
2567
|
+
}
|
|
2568
|
+
function pointerdown_handler(ev) {
|
|
2569
|
+
// The pointerdown event signals the start of a touch interaction.
|
|
2570
|
+
// This event is cached to support 2-finger gestures
|
|
2571
|
+
evCache.push(ev);
|
|
2572
|
+
}
|
|
2573
|
+
function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
|
|
2574
|
+
// This function implements a 2-pointer horizontal pinch/zoom gesture.
|
|
2575
|
+
//
|
|
2576
|
+
// If the distance between the two pointers has increased (zoom in),
|
|
2577
|
+
// the target element's background is changed to "pink" and if the
|
|
2578
|
+
// distance is decreasing (zoom out), the color is changed to "lightblue".
|
|
2579
|
+
//
|
|
2580
|
+
// Find this event in the cache and update its record with this event
|
|
2581
|
+
for (let i = 0; i < evCache.length; i++) {
|
|
2582
|
+
if (ev.pointerId === evCache[i].pointerId) {
|
|
2583
|
+
evCache[i] = ev;
|
|
2584
|
+
break;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
// If two pointers are down, check for pinch gestures
|
|
2588
|
+
if (evCache.length == 2) {
|
|
2589
|
+
// Calculate the distance between the two pointers
|
|
2590
|
+
const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
|
|
2591
|
+
Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
|
|
2592
|
+
if (origDistance > 0) {
|
|
2593
|
+
if (curDistance - origDistance >= PINCH_THRESHOLD) {
|
|
2594
|
+
// The distance between the two pointers has increased
|
|
2595
|
+
if (!hasZoomed)
|
|
2596
|
+
funcZoomIn();
|
|
2597
|
+
hasZoomed = true;
|
|
2598
|
+
}
|
|
2599
|
+
else if (origDistance - curDistance >= PINCH_THRESHOLD) {
|
|
2600
|
+
// The distance between the two pointers has decreased
|
|
2601
|
+
if (!hasZoomed)
|
|
2602
|
+
funcZoomOut();
|
|
2603
|
+
hasZoomed = true;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
else if (origDistance < 0) {
|
|
2607
|
+
// Note the original difference between two pointers
|
|
2608
|
+
origDistance = curDistance;
|
|
2609
|
+
hasZoomed = false;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
function pointerup_handler(ev) {
|
|
2614
|
+
// Remove this pointer from the cache and reset the target's
|
|
2615
|
+
// background and border
|
|
2616
|
+
remove_event(ev);
|
|
2617
|
+
// If the number of pointers down is less than two then reset diff tracker
|
|
2618
|
+
if (evCache.length < 2) {
|
|
2619
|
+
origDistance = -1;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function remove_event(ev) {
|
|
2623
|
+
// Remove this event from the target's cache
|
|
2624
|
+
for (let i = 0; i < evCache.length; i++) {
|
|
2625
|
+
if (evCache[i].pointerId === ev.pointerId) {
|
|
2626
|
+
evCache.splice(i, 1);
|
|
2627
|
+
break;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
function buttonOver(ev) {
|
|
2632
|
+
const clist = ev.currentTarget.classList;
|
|
2633
|
+
if (clist !== undefined && !clist.contains("disabled"))
|
|
2634
|
+
clist.add("over");
|
|
2635
|
+
ev.redraw = false;
|
|
2636
|
+
}
|
|
2637
|
+
function buttonOut(ev) {
|
|
2638
|
+
const clist = ev.currentTarget.classList;
|
|
2639
|
+
if (clist !== undefined)
|
|
2640
|
+
clist.remove("over");
|
|
2641
|
+
ev.redraw = false;
|
|
2642
|
+
}
|
|
2643
|
+
// Glyphicon utility function: inserts a glyphicon span
|
|
2644
|
+
function glyph(icon, attrs, grayed) {
|
|
2645
|
+
return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
|
|
2646
|
+
}
|
|
2647
|
+
function glyphGrayed(icon, attrs) {
|
|
2648
|
+
return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
|
|
2649
|
+
}
|
|
2650
|
+
// Utility function: inserts non-breaking space
|
|
2651
|
+
function nbsp(n) {
|
|
2652
|
+
return m.trust(" ");
|
|
2653
|
+
}
|
|
2654
|
+
// Utility functions
|
|
2655
|
+
function escapeHtml(string) {
|
|
2656
|
+
/* Utility function to properly encode a string into HTML */
|
|
2657
|
+
const entityMap = {
|
|
2658
|
+
"&": "&",
|
|
2659
|
+
"<": "<",
|
|
2660
|
+
">": ">",
|
|
2661
|
+
'"': '"',
|
|
2662
|
+
"'": ''',
|
|
2663
|
+
"/": '/'
|
|
2664
|
+
};
|
|
2665
|
+
return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
|
|
2666
|
+
}
|
|
2667
|
+
function getUrlVars(url) {
|
|
2668
|
+
// Get values from a URL query string
|
|
2669
|
+
const hashes = url.split('&');
|
|
2670
|
+
const vars = {};
|
|
2671
|
+
for (let i = 0; i < hashes.length; i++) {
|
|
2672
|
+
const hash = hashes[i].split('=');
|
|
2673
|
+
if (hash.length == 2)
|
|
2674
|
+
vars[hash[0]] = decodeURIComponent(hash[1]);
|
|
2675
|
+
}
|
|
2676
|
+
return vars;
|
|
2677
|
+
}
|
|
2678
|
+
function getInput(id) {
|
|
2679
|
+
// Return the current value of a text input field
|
|
2680
|
+
const elem = document.getElementById(id);
|
|
2681
|
+
return elem.value;
|
|
2682
|
+
}
|
|
2683
|
+
function setInput(id, val) {
|
|
2684
|
+
// Set the current value of a text input field
|
|
2685
|
+
const elem = document.getElementById(id);
|
|
2686
|
+
elem.value = val;
|
|
2687
|
+
}
|
|
2688
|
+
function playAudio(elemId) {
|
|
2689
|
+
// Play an audio file
|
|
2690
|
+
const sound = document.getElementById(elemId);
|
|
2691
|
+
if (sound)
|
|
2692
|
+
sound.play();
|
|
2693
|
+
}
|
|
2694
|
+
function arrayEqual(a, b) {
|
|
2695
|
+
// Return true if arrays a and b are equal
|
|
2696
|
+
if (a.length != b.length)
|
|
2697
|
+
return false;
|
|
2698
|
+
for (let i = 0; i < a.length; i++)
|
|
2699
|
+
if (a[i] != b[i])
|
|
2700
|
+
return false;
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
function gameUrl(url) {
|
|
2704
|
+
// Convert old-style game URL to new-style single-page URL
|
|
2705
|
+
// The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
|
|
2706
|
+
if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
|
|
2707
|
+
// Cut off "/board?game="
|
|
2708
|
+
url = url.slice(BOARD_PREFIX_LEN);
|
|
2709
|
+
// Isolate the game UUID
|
|
2710
|
+
const uuid = url.slice(0, 36);
|
|
2711
|
+
// Isolate the other parameters, if any
|
|
2712
|
+
let params = url.slice(36);
|
|
2713
|
+
// Start parameter section of URL with a ? sign
|
|
2714
|
+
if (params.length > 0 && params.charAt(0) == "&")
|
|
2715
|
+
params = "?" + params.slice(1);
|
|
2716
|
+
// Return the single-page URL, to be consumed by m.route.Link()
|
|
2717
|
+
return "/game/" + uuid + params;
|
|
2718
|
+
}
|
|
2719
|
+
function scrollMovelistToBottom() {
|
|
2720
|
+
// If the length of the move list has changed,
|
|
2721
|
+
// scroll the last move into view
|
|
2722
|
+
let movelist = document.querySelectorAll("div.movelist .move");
|
|
2723
|
+
if (!movelist || !movelist.length)
|
|
2724
|
+
return;
|
|
2725
|
+
let target = movelist[movelist.length - 1];
|
|
2726
|
+
let parent = target.parentNode;
|
|
2727
|
+
let len = parent.getAttribute("data-len");
|
|
2728
|
+
let intLen = (!len) ? 0 : parseInt(len);
|
|
2729
|
+
if (movelist.length > intLen) {
|
|
2730
|
+
// The list has grown since we last updated it:
|
|
2731
|
+
// scroll to the bottom and mark its length
|
|
2732
|
+
parent.scrollTop = target.offsetTop;
|
|
2733
|
+
}
|
|
2734
|
+
parent.setAttribute("data-len", movelist.length.toString());
|
|
2735
|
+
}
|
|
2736
|
+
function coord(row, col, vertical = false) {
|
|
2737
|
+
// Return the co-ordinate string for the given 0-based row and col
|
|
2738
|
+
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
|
|
2739
|
+
return null;
|
|
2740
|
+
// Horizontal moves have the row letter first
|
|
2741
|
+
// Vertical moves have the column number first
|
|
2742
|
+
return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
|
|
2743
|
+
}
|
|
2744
|
+
function toVector(co) {
|
|
2745
|
+
// Convert a co-ordinate string to a 0-based row, col and direction vector
|
|
2746
|
+
var dx = 0, dy = 0;
|
|
2747
|
+
var col = 0;
|
|
2748
|
+
var row = ROWIDS.indexOf(co[0]);
|
|
2749
|
+
if (row >= 0) {
|
|
2750
|
+
/* Horizontal move */
|
|
2751
|
+
col = parseInt(co.slice(1)) - 1;
|
|
2752
|
+
dx = 1;
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
/* Vertical move */
|
|
2756
|
+
row = ROWIDS.indexOf(co.slice(-1));
|
|
2757
|
+
col = parseInt(co) - 1;
|
|
2758
|
+
dy = 1;
|
|
2759
|
+
}
|
|
2760
|
+
return { col: col, row: row, dx: dx, dy: dy };
|
|
2761
|
+
}
|
|
2762
|
+
function valueOrK(value, breakpoint = 10000) {
|
|
2763
|
+
// Return a numeric value as a string, but in kilos (thousands)
|
|
2764
|
+
// if it exceeds a breakpoint, in that case suffixed by "K"
|
|
2765
|
+
const sign = value < 0 ? "-" : "";
|
|
2766
|
+
value = Math.abs(value);
|
|
2767
|
+
if (value < breakpoint)
|
|
2768
|
+
return `${sign}${value}`;
|
|
2769
|
+
value = Math.round(value / 1000);
|
|
2770
|
+
return `${sign}${value}K`;
|
|
2771
|
+
}
|
|
2772
|
+
// SalesCloud stuff
|
|
2773
|
+
function doRegisterSalesCloud(i, s, o, g, r, a, m) {
|
|
2774
|
+
i.SalesCloudObject = r;
|
|
2775
|
+
i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
|
|
2776
|
+
i[r].l = 1 * new Date();
|
|
2777
|
+
a = s.createElement(o);
|
|
2778
|
+
m = s.getElementsByTagName(o)[0];
|
|
2779
|
+
a.src = g;
|
|
2780
|
+
m.parentNode.insertBefore(a, m);
|
|
2781
|
+
}
|
|
2782
|
+
function registerSalesCloud() {
|
|
2783
|
+
doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2239
2786
|
/**
|
|
2240
2787
|
* @license
|
|
2241
2788
|
* Copyright 2017 Google LLC
|
|
@@ -26561,33 +27108,46 @@ let database;
|
|
|
26561
27108
|
let analytics;
|
|
26562
27109
|
function initFirebase(state) {
|
|
26563
27110
|
try {
|
|
26564
|
-
const projectId = state
|
|
27111
|
+
const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
|
|
26565
27112
|
const firebaseOptions = {
|
|
26566
27113
|
projectId,
|
|
26567
|
-
apiKey:
|
|
27114
|
+
apiKey: firebaseAPIKey,
|
|
26568
27115
|
authDomain: `${projectId}.firebaseapp.com`,
|
|
26569
|
-
databaseURL
|
|
27116
|
+
databaseURL,
|
|
26570
27117
|
storageBucket: `${projectId}.firebasestorage.app`,
|
|
26571
|
-
messagingSenderId:
|
|
26572
|
-
appId:
|
|
26573
|
-
measurementId
|
|
27118
|
+
messagingSenderId: firebaseSenderId,
|
|
27119
|
+
appId: firebaseAppId,
|
|
27120
|
+
measurementId,
|
|
26574
27121
|
};
|
|
26575
|
-
app = initializeApp(firebaseOptions,
|
|
27122
|
+
app = initializeApp(firebaseOptions, projectId);
|
|
26576
27123
|
if (!app) {
|
|
26577
27124
|
console.error("Failed to initialize Firebase");
|
|
27125
|
+
return false;
|
|
26578
27126
|
}
|
|
26579
27127
|
}
|
|
26580
27128
|
catch (e) {
|
|
26581
27129
|
console.error("Failed to initialize Firebase", e);
|
|
27130
|
+
return false;
|
|
27131
|
+
}
|
|
27132
|
+
return true;
|
|
27133
|
+
}
|
|
27134
|
+
function isFirebaseAuthenticated(state) {
|
|
27135
|
+
// Check if Firebase is currently authenticated
|
|
27136
|
+
// If auth is not initialized but state is provided, try to initialize
|
|
27137
|
+
if (!auth) {
|
|
27138
|
+
if (!app)
|
|
27139
|
+
initFirebase(state);
|
|
27140
|
+
if (app)
|
|
27141
|
+
auth = getAuth(app);
|
|
26582
27142
|
}
|
|
27143
|
+
if (!auth)
|
|
27144
|
+
return false;
|
|
27145
|
+
return auth.currentUser !== null;
|
|
26583
27146
|
}
|
|
26584
27147
|
async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
26585
|
-
if (!app)
|
|
26586
|
-
initFirebase(state);
|
|
26587
|
-
if (!app)
|
|
27148
|
+
if (!app && !initFirebase(state))
|
|
26588
27149
|
return;
|
|
26589
|
-
const userId = state
|
|
26590
|
-
const locale = state.locale;
|
|
27150
|
+
const { userId, locale, projectId, loginMethod } = state;
|
|
26591
27151
|
auth = getAuth(app);
|
|
26592
27152
|
if (!auth) {
|
|
26593
27153
|
console.error("Failed to initialize Firebase Auth");
|
|
@@ -26600,16 +27160,16 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
|
26600
27160
|
// For new users, log an additional signup event
|
|
26601
27161
|
if (state.newUser) {
|
|
26602
27162
|
logEvent("sign_up", {
|
|
26603
|
-
locale
|
|
26604
|
-
method:
|
|
26605
|
-
userid:
|
|
27163
|
+
locale,
|
|
27164
|
+
method: loginMethod,
|
|
27165
|
+
userid: userId
|
|
26606
27166
|
});
|
|
26607
27167
|
}
|
|
26608
27168
|
// And always log a login event
|
|
26609
27169
|
logEvent("login", {
|
|
26610
|
-
locale
|
|
26611
|
-
method:
|
|
26612
|
-
userid:
|
|
27170
|
+
locale,
|
|
27171
|
+
method: loginMethod,
|
|
27172
|
+
userid: userId
|
|
26613
27173
|
});
|
|
26614
27174
|
}
|
|
26615
27175
|
});
|
|
@@ -26624,7 +27184,7 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
|
|
|
26624
27184
|
if (!analytics) {
|
|
26625
27185
|
console.error("Failed to initialize Firebase Analytics");
|
|
26626
27186
|
}
|
|
26627
|
-
initPresence(
|
|
27187
|
+
initPresence(projectId, userId, locale);
|
|
26628
27188
|
}
|
|
26629
27189
|
function initPresence(projectId, userId, locale) {
|
|
26630
27190
|
// Ensure that this user connection is recorded in Firebase
|
|
@@ -26681,473 +27241,6 @@ function logEvent(ev, params) {
|
|
|
26681
27241
|
logEvent$2(analytics, ev, params);
|
|
26682
27242
|
}
|
|
26683
27243
|
|
|
26684
|
-
/*
|
|
26685
|
-
|
|
26686
|
-
i8n.ts
|
|
26687
|
-
|
|
26688
|
-
Single page UI for Netskrafl/Explo using the Mithril library
|
|
26689
|
-
|
|
26690
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26691
|
-
Author: Vilhjálmur Þorsteinsson
|
|
26692
|
-
|
|
26693
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26694
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26695
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26696
|
-
|
|
26697
|
-
|
|
26698
|
-
This module contains internationalization (i18n) utility functions,
|
|
26699
|
-
allowing for translation of displayed text between languages.
|
|
26700
|
-
|
|
26701
|
-
Text messages for individual locales are loaded from the
|
|
26702
|
-
/static/assets/messages.json file, which is fetched from the server.
|
|
26703
|
-
|
|
26704
|
-
*/
|
|
26705
|
-
// Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
|
|
26706
|
-
// This is overwritten in setLocale()
|
|
26707
|
-
let currentLocale = "is_IS";
|
|
26708
|
-
let currentFallback = "is";
|
|
26709
|
-
// Regex that matches embedded interpolations such as "Welcome, {username}!"
|
|
26710
|
-
// Interpolation identifiers should only contain ASCII characters, digits and '_'
|
|
26711
|
-
const rex = /{\s*(\w+)\s*}/g;
|
|
26712
|
-
let messages = {};
|
|
26713
|
-
let messagesLoaded = false;
|
|
26714
|
-
function hasAnyTranslation(msgs, locale) {
|
|
26715
|
-
// Return true if any translation is available for the given locale
|
|
26716
|
-
for (let key in msgs) {
|
|
26717
|
-
if (msgs[key][locale] !== undefined)
|
|
26718
|
-
return true;
|
|
26719
|
-
}
|
|
26720
|
-
return false;
|
|
26721
|
-
}
|
|
26722
|
-
function setLocale(locale, msgs) {
|
|
26723
|
-
// Set the current i18n locale and fallback
|
|
26724
|
-
currentLocale = locale;
|
|
26725
|
-
currentFallback = locale.split("_")[0];
|
|
26726
|
-
// For unsupported locales, i.e. locales that have no
|
|
26727
|
-
// translations available for them, fall back to English (U.S.).
|
|
26728
|
-
if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
|
|
26729
|
-
currentLocale = "en_US";
|
|
26730
|
-
currentFallback = "en";
|
|
26731
|
-
}
|
|
26732
|
-
// Flatten the Messages structure, enabling long strings
|
|
26733
|
-
// to be represented as string arrays in the messages.json file
|
|
26734
|
-
messages = {};
|
|
26735
|
-
for (let key in msgs) {
|
|
26736
|
-
for (let lc in msgs[key]) {
|
|
26737
|
-
let s = msgs[key][lc];
|
|
26738
|
-
if (Array.isArray(s))
|
|
26739
|
-
s = s.join("");
|
|
26740
|
-
if (messages[key] === undefined)
|
|
26741
|
-
messages[key] = {};
|
|
26742
|
-
// If the string s contains HTML markup of the form <tag>...</tag>,
|
|
26743
|
-
// convert it into a list of Mithril Vnode children corresponding to
|
|
26744
|
-
// the text and the tags
|
|
26745
|
-
if (s.match(/<[a-z]+>/)) {
|
|
26746
|
-
// Looks like the string contains HTML markup
|
|
26747
|
-
const vnodes = [];
|
|
26748
|
-
let i = 0;
|
|
26749
|
-
let tagMatch = null;
|
|
26750
|
-
while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
|
|
26751
|
-
// Found what looks like an HTML tag
|
|
26752
|
-
// Calculate the index of the enclosed text within s
|
|
26753
|
-
const tag = tagMatch[0];
|
|
26754
|
-
let j = i + tagMatch.index + tag.length;
|
|
26755
|
-
// Find the end tag
|
|
26756
|
-
let end = s.indexOf("</" + tag.slice(1), j);
|
|
26757
|
-
if (end < 0) {
|
|
26758
|
-
// No end tag - skip past this weirdness
|
|
26759
|
-
i = j;
|
|
26760
|
-
continue;
|
|
26761
|
-
}
|
|
26762
|
-
// Add the text preceding the tag
|
|
26763
|
-
if (tagMatch.index > 0)
|
|
26764
|
-
vnodes.push(s.slice(i, i + tagMatch.index));
|
|
26765
|
-
// Create the Mithril node corresponding to the tag and the enclosed text
|
|
26766
|
-
// and add it to the list
|
|
26767
|
-
vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
|
|
26768
|
-
// Advance the index past the end of the tag
|
|
26769
|
-
i = end + tag.length + 1;
|
|
26770
|
-
}
|
|
26771
|
-
// Push the final text part, if any
|
|
26772
|
-
if (i < s.length)
|
|
26773
|
-
vnodes.push(s.slice(i));
|
|
26774
|
-
// Reassign s to the list of vnodes
|
|
26775
|
-
s = vnodes;
|
|
26776
|
-
}
|
|
26777
|
-
messages[key][lc] = s;
|
|
26778
|
-
}
|
|
26779
|
-
}
|
|
26780
|
-
messagesLoaded = true;
|
|
26781
|
-
}
|
|
26782
|
-
async function loadMessages(locale) {
|
|
26783
|
-
// Load the internationalization message JSON file from the server
|
|
26784
|
-
// and set the user's locale
|
|
26785
|
-
try {
|
|
26786
|
-
const messages = await request({
|
|
26787
|
-
method: "GET",
|
|
26788
|
-
url: "/static/assets/messages.json",
|
|
26789
|
-
withCredentials: false, // Cookies are not allowed for CORS request
|
|
26790
|
-
});
|
|
26791
|
-
setLocale(locale, messages);
|
|
26792
|
-
}
|
|
26793
|
-
catch (_a) {
|
|
26794
|
-
setLocale(locale, {});
|
|
26795
|
-
}
|
|
26796
|
-
}
|
|
26797
|
-
function t(key, ips = {}) {
|
|
26798
|
-
// Main text translation function, supporting interpolation
|
|
26799
|
-
// and HTML tag substitution
|
|
26800
|
-
const msgDict = messages[key];
|
|
26801
|
-
if (msgDict === undefined)
|
|
26802
|
-
// No dictionary for this key - may actually be a missing entry
|
|
26803
|
-
return messagesLoaded ? key : "";
|
|
26804
|
-
// Lookup exact locale, then fallback, then resort to returning the key
|
|
26805
|
-
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
26806
|
-
// If we have an interpolation object, do the interpolation first
|
|
26807
|
-
return Object.keys(ips).length ? interpolate(message, ips) : message;
|
|
26808
|
-
}
|
|
26809
|
-
function ts(key, ips = {}) {
|
|
26810
|
-
// String translation function, supporting interpolation
|
|
26811
|
-
// but not HTML tag substitution
|
|
26812
|
-
const msgDict = messages[key];
|
|
26813
|
-
if (msgDict === undefined)
|
|
26814
|
-
// No dictionary for this key - may actually be a missing entry
|
|
26815
|
-
return messagesLoaded ? key : "";
|
|
26816
|
-
// Lookup exact locale, then fallback, then resort to returning the key
|
|
26817
|
-
const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
|
|
26818
|
-
if (typeof message != "string")
|
|
26819
|
-
// This is actually an error - the client should be calling t() instead
|
|
26820
|
-
return "";
|
|
26821
|
-
// If we have an interpolation object, do the interpolation first
|
|
26822
|
-
return Object.keys(ips).length ? interpolate_string(message, ips) : message;
|
|
26823
|
-
}
|
|
26824
|
-
function mt(cls, children) {
|
|
26825
|
-
// Wrapper for the Mithril m() function that auto-translates
|
|
26826
|
-
// string and array arguments
|
|
26827
|
-
if (typeof children == "string") {
|
|
26828
|
-
return m(cls, t(children));
|
|
26829
|
-
}
|
|
26830
|
-
if (Array.isArray(children)) {
|
|
26831
|
-
return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
|
|
26832
|
-
}
|
|
26833
|
-
return m(cls, children);
|
|
26834
|
-
}
|
|
26835
|
-
function interpolate(message, ips) {
|
|
26836
|
-
// Replace interpolation placeholders with their corresponding values
|
|
26837
|
-
if (typeof message == "string") {
|
|
26838
|
-
return message.replace(rex, (match, key) => ips[key] || match);
|
|
26839
|
-
}
|
|
26840
|
-
if (Array.isArray(message)) {
|
|
26841
|
-
return message.map((item) => interpolate(item, ips));
|
|
26842
|
-
}
|
|
26843
|
-
return message;
|
|
26844
|
-
}
|
|
26845
|
-
function interpolate_string(message, ips) {
|
|
26846
|
-
// Replace interpolation placeholders with their corresponding values
|
|
26847
|
-
return message.replace(rex, (match, key) => ips[key] || match);
|
|
26848
|
-
}
|
|
26849
|
-
|
|
26850
|
-
/*
|
|
26851
|
-
|
|
26852
|
-
Types.ts
|
|
26853
|
-
|
|
26854
|
-
Common type definitions for the Explo/Netskrafl user interface
|
|
26855
|
-
|
|
26856
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26857
|
-
Author: Vilhjalmur Thorsteinsson
|
|
26858
|
-
|
|
26859
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26860
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26861
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26862
|
-
|
|
26863
|
-
*/
|
|
26864
|
-
// Global constants
|
|
26865
|
-
const RACK_SIZE = 7;
|
|
26866
|
-
const ROWIDS = "ABCDEFGHIJKLMNO";
|
|
26867
|
-
const BOARD_SIZE = ROWIDS.length;
|
|
26868
|
-
const EXTRA_WIDE_LETTERS = "q";
|
|
26869
|
-
const WIDE_LETTERS = "zxmæ";
|
|
26870
|
-
const ZOOM_FACTOR = 1.5;
|
|
26871
|
-
const ERROR_MESSAGES = {
|
|
26872
|
-
// Translations are found in /static/assets/messages.json
|
|
26873
|
-
1: "Enginn stafur lagður niður",
|
|
26874
|
-
2: "Fyrsta orð verður að liggja um byrjunarreitinn",
|
|
26875
|
-
3: "Orð verður að vera samfellt á borðinu",
|
|
26876
|
-
4: "Orð verður að tengjast orði sem fyrir er",
|
|
26877
|
-
5: "Reitur þegar upptekinn",
|
|
26878
|
-
6: "Ekki má vera eyða í orði",
|
|
26879
|
-
7: "word_not_found",
|
|
26880
|
-
8: "word_not_found",
|
|
26881
|
-
9: "Of margir stafir lagðir niður",
|
|
26882
|
-
10: "Stafur er ekki í rekkanum",
|
|
26883
|
-
11: "Of fáir stafir eftir, skipting ekki leyfð",
|
|
26884
|
-
12: "Of mörgum stöfum skipt",
|
|
26885
|
-
13: "Leik vantar á borðið - notið F5/Refresh",
|
|
26886
|
-
14: "Notandi ekki innskráður - notið F5/Refresh",
|
|
26887
|
-
15: "Rangur eða óþekktur notandi",
|
|
26888
|
-
16: "Viðureign finnst ekki",
|
|
26889
|
-
17: "Viðureign er ekki utan tímamarka",
|
|
26890
|
-
18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
|
|
26891
|
-
19: "Véfenging er ekki möguleg í þessari viðureign",
|
|
26892
|
-
20: "Síðasti leikur er ekki véfengjanlegur",
|
|
26893
|
-
21: "Aðeins véfenging eða pass leyfileg",
|
|
26894
|
-
"server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
|
|
26895
|
-
};
|
|
26896
|
-
|
|
26897
|
-
/*
|
|
26898
|
-
|
|
26899
|
-
Util.ts
|
|
26900
|
-
|
|
26901
|
-
Utility functions for the Explo/Netskrafl user interface
|
|
26902
|
-
|
|
26903
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
26904
|
-
Author: Vilhjálmur Þorsteinsson
|
|
26905
|
-
|
|
26906
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
26907
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
26908
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
26909
|
-
|
|
26910
|
-
The following code is based on
|
|
26911
|
-
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
|
|
26912
|
-
|
|
26913
|
-
*/
|
|
26914
|
-
// Global vars to cache event state
|
|
26915
|
-
var evCache = [];
|
|
26916
|
-
var origDistance = -1;
|
|
26917
|
-
const PINCH_THRESHOLD = 10; // Minimum pinch movement
|
|
26918
|
-
var hasZoomed = false;
|
|
26919
|
-
// Old-style (non-single-page) game URL prefix
|
|
26920
|
-
const BOARD_PREFIX = "/board?game=";
|
|
26921
|
-
const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
|
|
26922
|
-
function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
|
|
26923
|
-
// Install event handlers for the pointer target
|
|
26924
|
-
attrs.onpointerdown = pointerdown_handler;
|
|
26925
|
-
attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
|
|
26926
|
-
// Use same handler for pointer{up,cancel,out,leave} events since
|
|
26927
|
-
// the semantics for these events - in this app - are the same.
|
|
26928
|
-
attrs.onpointerup = pointerup_handler;
|
|
26929
|
-
attrs.onpointercancel = pointerup_handler;
|
|
26930
|
-
attrs.onpointerout = pointerup_handler;
|
|
26931
|
-
attrs.onpointerleave = pointerup_handler;
|
|
26932
|
-
}
|
|
26933
|
-
function pointerdown_handler(ev) {
|
|
26934
|
-
// The pointerdown event signals the start of a touch interaction.
|
|
26935
|
-
// This event is cached to support 2-finger gestures
|
|
26936
|
-
evCache.push(ev);
|
|
26937
|
-
}
|
|
26938
|
-
function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
|
|
26939
|
-
// This function implements a 2-pointer horizontal pinch/zoom gesture.
|
|
26940
|
-
//
|
|
26941
|
-
// If the distance between the two pointers has increased (zoom in),
|
|
26942
|
-
// the target element's background is changed to "pink" and if the
|
|
26943
|
-
// distance is decreasing (zoom out), the color is changed to "lightblue".
|
|
26944
|
-
//
|
|
26945
|
-
// Find this event in the cache and update its record with this event
|
|
26946
|
-
for (let i = 0; i < evCache.length; i++) {
|
|
26947
|
-
if (ev.pointerId === evCache[i].pointerId) {
|
|
26948
|
-
evCache[i] = ev;
|
|
26949
|
-
break;
|
|
26950
|
-
}
|
|
26951
|
-
}
|
|
26952
|
-
// If two pointers are down, check for pinch gestures
|
|
26953
|
-
if (evCache.length == 2) {
|
|
26954
|
-
// Calculate the distance between the two pointers
|
|
26955
|
-
const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
|
|
26956
|
-
Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
|
|
26957
|
-
if (origDistance > 0) {
|
|
26958
|
-
if (curDistance - origDistance >= PINCH_THRESHOLD) {
|
|
26959
|
-
// The distance between the two pointers has increased
|
|
26960
|
-
if (!hasZoomed)
|
|
26961
|
-
funcZoomIn();
|
|
26962
|
-
hasZoomed = true;
|
|
26963
|
-
}
|
|
26964
|
-
else if (origDistance - curDistance >= PINCH_THRESHOLD) {
|
|
26965
|
-
// The distance between the two pointers has decreased
|
|
26966
|
-
if (!hasZoomed)
|
|
26967
|
-
funcZoomOut();
|
|
26968
|
-
hasZoomed = true;
|
|
26969
|
-
}
|
|
26970
|
-
}
|
|
26971
|
-
else if (origDistance < 0) {
|
|
26972
|
-
// Note the original difference between two pointers
|
|
26973
|
-
origDistance = curDistance;
|
|
26974
|
-
hasZoomed = false;
|
|
26975
|
-
}
|
|
26976
|
-
}
|
|
26977
|
-
}
|
|
26978
|
-
function pointerup_handler(ev) {
|
|
26979
|
-
// Remove this pointer from the cache and reset the target's
|
|
26980
|
-
// background and border
|
|
26981
|
-
remove_event(ev);
|
|
26982
|
-
// If the number of pointers down is less than two then reset diff tracker
|
|
26983
|
-
if (evCache.length < 2) {
|
|
26984
|
-
origDistance = -1;
|
|
26985
|
-
}
|
|
26986
|
-
}
|
|
26987
|
-
function remove_event(ev) {
|
|
26988
|
-
// Remove this event from the target's cache
|
|
26989
|
-
for (let i = 0; i < evCache.length; i++) {
|
|
26990
|
-
if (evCache[i].pointerId === ev.pointerId) {
|
|
26991
|
-
evCache.splice(i, 1);
|
|
26992
|
-
break;
|
|
26993
|
-
}
|
|
26994
|
-
}
|
|
26995
|
-
}
|
|
26996
|
-
function buttonOver(ev) {
|
|
26997
|
-
const clist = ev.currentTarget.classList;
|
|
26998
|
-
if (clist !== undefined && !clist.contains("disabled"))
|
|
26999
|
-
clist.add("over");
|
|
27000
|
-
ev.redraw = false;
|
|
27001
|
-
}
|
|
27002
|
-
function buttonOut(ev) {
|
|
27003
|
-
const clist = ev.currentTarget.classList;
|
|
27004
|
-
if (clist !== undefined)
|
|
27005
|
-
clist.remove("over");
|
|
27006
|
-
ev.redraw = false;
|
|
27007
|
-
}
|
|
27008
|
-
// Glyphicon utility function: inserts a glyphicon span
|
|
27009
|
-
function glyph(icon, attrs, grayed) {
|
|
27010
|
-
return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
|
|
27011
|
-
}
|
|
27012
|
-
function glyphGrayed(icon, attrs) {
|
|
27013
|
-
return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
|
|
27014
|
-
}
|
|
27015
|
-
// Utility function: inserts non-breaking space
|
|
27016
|
-
function nbsp(n) {
|
|
27017
|
-
return m.trust(" ");
|
|
27018
|
-
}
|
|
27019
|
-
// Utility functions
|
|
27020
|
-
function escapeHtml(string) {
|
|
27021
|
-
/* Utility function to properly encode a string into HTML */
|
|
27022
|
-
const entityMap = {
|
|
27023
|
-
"&": "&",
|
|
27024
|
-
"<": "<",
|
|
27025
|
-
">": ">",
|
|
27026
|
-
'"': '"',
|
|
27027
|
-
"'": ''',
|
|
27028
|
-
"/": '/'
|
|
27029
|
-
};
|
|
27030
|
-
return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
|
|
27031
|
-
}
|
|
27032
|
-
function getUrlVars(url) {
|
|
27033
|
-
// Get values from a URL query string
|
|
27034
|
-
const hashes = url.split('&');
|
|
27035
|
-
const vars = {};
|
|
27036
|
-
for (let i = 0; i < hashes.length; i++) {
|
|
27037
|
-
const hash = hashes[i].split('=');
|
|
27038
|
-
if (hash.length == 2)
|
|
27039
|
-
vars[hash[0]] = decodeURIComponent(hash[1]);
|
|
27040
|
-
}
|
|
27041
|
-
return vars;
|
|
27042
|
-
}
|
|
27043
|
-
function getInput(id) {
|
|
27044
|
-
// Return the current value of a text input field
|
|
27045
|
-
const elem = document.getElementById(id);
|
|
27046
|
-
return elem.value;
|
|
27047
|
-
}
|
|
27048
|
-
function setInput(id, val) {
|
|
27049
|
-
// Set the current value of a text input field
|
|
27050
|
-
const elem = document.getElementById(id);
|
|
27051
|
-
elem.value = val;
|
|
27052
|
-
}
|
|
27053
|
-
function playAudio(elemId) {
|
|
27054
|
-
// Play an audio file
|
|
27055
|
-
const sound = document.getElementById(elemId);
|
|
27056
|
-
if (sound)
|
|
27057
|
-
sound.play();
|
|
27058
|
-
}
|
|
27059
|
-
function arrayEqual(a, b) {
|
|
27060
|
-
// Return true if arrays a and b are equal
|
|
27061
|
-
if (a.length != b.length)
|
|
27062
|
-
return false;
|
|
27063
|
-
for (let i = 0; i < a.length; i++)
|
|
27064
|
-
if (a[i] != b[i])
|
|
27065
|
-
return false;
|
|
27066
|
-
return true;
|
|
27067
|
-
}
|
|
27068
|
-
function gameUrl(url) {
|
|
27069
|
-
// Convert old-style game URL to new-style single-page URL
|
|
27070
|
-
// The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
|
|
27071
|
-
if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
|
|
27072
|
-
// Cut off "/board?game="
|
|
27073
|
-
url = url.slice(BOARD_PREFIX_LEN);
|
|
27074
|
-
// Isolate the game UUID
|
|
27075
|
-
const uuid = url.slice(0, 36);
|
|
27076
|
-
// Isolate the other parameters, if any
|
|
27077
|
-
let params = url.slice(36);
|
|
27078
|
-
// Start parameter section of URL with a ? sign
|
|
27079
|
-
if (params.length > 0 && params.charAt(0) == "&")
|
|
27080
|
-
params = "?" + params.slice(1);
|
|
27081
|
-
// Return the single-page URL, to be consumed by m.route.Link()
|
|
27082
|
-
return "/game/" + uuid + params;
|
|
27083
|
-
}
|
|
27084
|
-
function scrollMovelistToBottom() {
|
|
27085
|
-
// If the length of the move list has changed,
|
|
27086
|
-
// scroll the last move into view
|
|
27087
|
-
let movelist = document.querySelectorAll("div.movelist .move");
|
|
27088
|
-
if (!movelist || !movelist.length)
|
|
27089
|
-
return;
|
|
27090
|
-
let target = movelist[movelist.length - 1];
|
|
27091
|
-
let parent = target.parentNode;
|
|
27092
|
-
let len = parent.getAttribute("data-len");
|
|
27093
|
-
let intLen = (!len) ? 0 : parseInt(len);
|
|
27094
|
-
if (movelist.length > intLen) {
|
|
27095
|
-
// The list has grown since we last updated it:
|
|
27096
|
-
// scroll to the bottom and mark its length
|
|
27097
|
-
parent.scrollTop = target.offsetTop;
|
|
27098
|
-
}
|
|
27099
|
-
parent.setAttribute("data-len", movelist.length.toString());
|
|
27100
|
-
}
|
|
27101
|
-
function coord(row, col, vertical = false) {
|
|
27102
|
-
// Return the co-ordinate string for the given 0-based row and col
|
|
27103
|
-
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
|
|
27104
|
-
return null;
|
|
27105
|
-
// Horizontal moves have the row letter first
|
|
27106
|
-
// Vertical moves have the column number first
|
|
27107
|
-
return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
|
|
27108
|
-
}
|
|
27109
|
-
function toVector(co) {
|
|
27110
|
-
// Convert a co-ordinate string to a 0-based row, col and direction vector
|
|
27111
|
-
var dx = 0, dy = 0;
|
|
27112
|
-
var col = 0;
|
|
27113
|
-
var row = ROWIDS.indexOf(co[0]);
|
|
27114
|
-
if (row >= 0) {
|
|
27115
|
-
/* Horizontal move */
|
|
27116
|
-
col = parseInt(co.slice(1)) - 1;
|
|
27117
|
-
dx = 1;
|
|
27118
|
-
}
|
|
27119
|
-
else {
|
|
27120
|
-
/* Vertical move */
|
|
27121
|
-
row = ROWIDS.indexOf(co.slice(-1));
|
|
27122
|
-
col = parseInt(co) - 1;
|
|
27123
|
-
dy = 1;
|
|
27124
|
-
}
|
|
27125
|
-
return { col: col, row: row, dx: dx, dy: dy };
|
|
27126
|
-
}
|
|
27127
|
-
function valueOrK(value, breakpoint = 10000) {
|
|
27128
|
-
// Return a numeric value as a string, but in kilos (thousands)
|
|
27129
|
-
// if it exceeds a breakpoint, in that case suffixed by "K"
|
|
27130
|
-
const sign = value < 0 ? "-" : "";
|
|
27131
|
-
value = Math.abs(value);
|
|
27132
|
-
if (value < breakpoint)
|
|
27133
|
-
return `${sign}${value}`;
|
|
27134
|
-
value = Math.round(value / 1000);
|
|
27135
|
-
return `${sign}${value}K`;
|
|
27136
|
-
}
|
|
27137
|
-
// SalesCloud stuff
|
|
27138
|
-
function doRegisterSalesCloud(i, s, o, g, r, a, m) {
|
|
27139
|
-
i.SalesCloudObject = r;
|
|
27140
|
-
i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
|
|
27141
|
-
i[r].l = 1 * new Date();
|
|
27142
|
-
a = s.createElement(o);
|
|
27143
|
-
m = s.getElementsByTagName(o)[0];
|
|
27144
|
-
a.src = g;
|
|
27145
|
-
m.parentNode.insertBefore(a, m);
|
|
27146
|
-
}
|
|
27147
|
-
function registerSalesCloud() {
|
|
27148
|
-
doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
|
|
27149
|
-
}
|
|
27150
|
-
|
|
27151
27244
|
/*
|
|
27152
27245
|
|
|
27153
27246
|
Logo.ts
|
|
@@ -27257,7 +27350,7 @@ const NetskraflLegend = (initialVnode) => {
|
|
|
27257
27350
|
m.redraw();
|
|
27258
27351
|
}
|
|
27259
27352
|
return {
|
|
27260
|
-
|
|
27353
|
+
oninit: () => {
|
|
27261
27354
|
if (msStepTime && ival === 0) {
|
|
27262
27355
|
ival = setInterval(doStep, msStepTime);
|
|
27263
27356
|
}
|
|
@@ -27312,10 +27405,224 @@ const AnimatedNetskraflLogo = (initialVnode) => {
|
|
|
27312
27405
|
};
|
|
27313
27406
|
};
|
|
27314
27407
|
|
|
27408
|
+
/*
|
|
27409
|
+
|
|
27410
|
+
Login.ts
|
|
27411
|
+
|
|
27412
|
+
Login UI for Metskrafl using the Mithril library
|
|
27413
|
+
|
|
27414
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
27415
|
+
Author: Vilhjálmur Þorsteinsson
|
|
27416
|
+
|
|
27417
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
27418
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
27419
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
27420
|
+
|
|
27421
|
+
This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
|
|
27422
|
+
straightforward JavaScript single-page reactive UI library.
|
|
27423
|
+
|
|
27424
|
+
*/
|
|
27425
|
+
const loginUserByEmail = async (state) => {
|
|
27426
|
+
// Call the /login_malstadur endpoint on the server
|
|
27427
|
+
// to log in the user with the given email and token.
|
|
27428
|
+
// The token is a standard HS256-encoded JWT with aud "netskrafl"
|
|
27429
|
+
// and iss typically "malstadur".
|
|
27430
|
+
const { userEmail, userNick, userFullname, token } = state;
|
|
27431
|
+
return requestWithoutAuth(state, {
|
|
27432
|
+
method: "POST",
|
|
27433
|
+
url: "/login_malstadur",
|
|
27434
|
+
body: { email: userEmail, nickname: userNick, fullname: userFullname, token }
|
|
27435
|
+
});
|
|
27436
|
+
};
|
|
27437
|
+
const LoginError = {
|
|
27438
|
+
view: (vnode) => {
|
|
27439
|
+
return m("div.error", {
|
|
27440
|
+
style: { visibility: "visible" }
|
|
27441
|
+
}, vnode.children);
|
|
27442
|
+
}
|
|
27443
|
+
};
|
|
27444
|
+
const LoginForm = (initialVnode) => {
|
|
27445
|
+
const loginUrl = initialVnode.attrs.loginUrl;
|
|
27446
|
+
let loginInProgress = false;
|
|
27447
|
+
function doLogin(ev) {
|
|
27448
|
+
loginInProgress = true;
|
|
27449
|
+
ev.preventDefault();
|
|
27450
|
+
window.location.href = loginUrl;
|
|
27451
|
+
}
|
|
27452
|
+
return {
|
|
27453
|
+
view: () => {
|
|
27454
|
+
return m.fragment({}, [
|
|
27455
|
+
// This is visible on large screens
|
|
27456
|
+
m("div.loginform-large", [
|
|
27457
|
+
m(NetskraflLogoOnly, {
|
|
27458
|
+
className: "login-logo",
|
|
27459
|
+
width: 200,
|
|
27460
|
+
}),
|
|
27461
|
+
m(NetskraflLegend, {
|
|
27462
|
+
className: "login-legend",
|
|
27463
|
+
width: 600,
|
|
27464
|
+
msStepTime: 0
|
|
27465
|
+
}),
|
|
27466
|
+
mt("div.welcome", "welcome_0"),
|
|
27467
|
+
mt("div.welcome", "welcome_1"),
|
|
27468
|
+
mt("div.welcome", "welcome_2"),
|
|
27469
|
+
m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
|
|
27470
|
+
t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
|
|
27471
|
+
])
|
|
27472
|
+
]),
|
|
27473
|
+
// This is visible on small screens
|
|
27474
|
+
m("div.loginform-small", [
|
|
27475
|
+
m(NetskraflLogoOnly, {
|
|
27476
|
+
className: "login-logo",
|
|
27477
|
+
width: 160,
|
|
27478
|
+
}),
|
|
27479
|
+
m(NetskraflLegend, {
|
|
27480
|
+
className: "login-legend",
|
|
27481
|
+
width: 650,
|
|
27482
|
+
msStepTime: 0
|
|
27483
|
+
}),
|
|
27484
|
+
m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
|
|
27485
|
+
])
|
|
27486
|
+
]);
|
|
27487
|
+
}
|
|
27488
|
+
};
|
|
27489
|
+
};
|
|
27490
|
+
|
|
27491
|
+
// Global state for authentication
|
|
27492
|
+
let authPromise = null;
|
|
27493
|
+
// Custom error class for authentication failures
|
|
27494
|
+
class AuthenticationError extends Error {
|
|
27495
|
+
constructor() {
|
|
27496
|
+
super("Authentication required");
|
|
27497
|
+
this.name = "AuthenticationError";
|
|
27498
|
+
}
|
|
27499
|
+
}
|
|
27500
|
+
// Internal function to ensure authentication
|
|
27501
|
+
const ensureAuthenticated = async (state) => {
|
|
27502
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
27503
|
+
// If login is already in progress, wait for it to complete
|
|
27504
|
+
if (authPromise) {
|
|
27505
|
+
await authPromise;
|
|
27506
|
+
return;
|
|
27507
|
+
}
|
|
27508
|
+
// Start new login attempt (either forced by 401 or needed for Firebase)
|
|
27509
|
+
authPromise = loginUserByEmail(state);
|
|
27510
|
+
try {
|
|
27511
|
+
const result = await authPromise;
|
|
27512
|
+
if (result.status === "expired") {
|
|
27513
|
+
// Token has expired, notify the React component if callback is set
|
|
27514
|
+
state.tokenExpired && state.tokenExpired();
|
|
27515
|
+
// Clear any persisted settings since they're no longer valid
|
|
27516
|
+
clearAuthSettings();
|
|
27517
|
+
throw new Error("Token expired");
|
|
27518
|
+
}
|
|
27519
|
+
else if (result.status !== "success") {
|
|
27520
|
+
// Clear any persisted settings on auth failure
|
|
27521
|
+
clearAuthSettings();
|
|
27522
|
+
throw new Error(`Authentication failed: ${result.message || result.status}`);
|
|
27523
|
+
}
|
|
27524
|
+
// Update the user's ID to the internal one used by the backend and Firebase
|
|
27525
|
+
state.userId = result.user_id || state.userId;
|
|
27526
|
+
// Update the user's nickname
|
|
27527
|
+
state.userNick = result.nickname || state.userNick;
|
|
27528
|
+
// Use the server's Firebase API key, if provided
|
|
27529
|
+
state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
|
|
27530
|
+
// Load state flags and preferences
|
|
27531
|
+
state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
|
|
27532
|
+
state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
|
|
27533
|
+
state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
|
|
27534
|
+
state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
|
|
27535
|
+
// Save the authentication settings to sessionStorage for persistence
|
|
27536
|
+
saveAuthSettings({
|
|
27537
|
+
userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
|
|
27538
|
+
userId: state.userId,
|
|
27539
|
+
userNick: state.userNick,
|
|
27540
|
+
firebaseAPIKey: state.firebaseAPIKey,
|
|
27541
|
+
beginner: state.beginner,
|
|
27542
|
+
fairPlay: state.fairPlay,
|
|
27543
|
+
ready: state.ready,
|
|
27544
|
+
readyTimed: state.readyTimed,
|
|
27545
|
+
});
|
|
27546
|
+
// Success: Log in to Firebase with the token passed from the server
|
|
27547
|
+
await loginFirebase(state, result.firebase_token);
|
|
27548
|
+
}
|
|
27549
|
+
finally {
|
|
27550
|
+
// Reset the promise so future 401s can trigger a new login
|
|
27551
|
+
authPromise = null;
|
|
27552
|
+
}
|
|
27553
|
+
};
|
|
27554
|
+
// Internal authenticated request function
|
|
27555
|
+
const authenticatedRequest = async (state, options, retries = 0) => {
|
|
27556
|
+
// Before making the request, check if Firebase needs authentication
|
|
27557
|
+
// This handles the case where the user returns with a valid backend cookie but expired Firebase auth
|
|
27558
|
+
if (!retries && !isFirebaseAuthenticated(state)) {
|
|
27559
|
+
// Firebase is not authenticated, ensure authentication before making the request
|
|
27560
|
+
await ensureAuthenticated(state);
|
|
27561
|
+
}
|
|
27562
|
+
try {
|
|
27563
|
+
// Make the actual request
|
|
27564
|
+
// console.log("Making authenticated request to", options.url);
|
|
27565
|
+
return await m.request({
|
|
27566
|
+
withCredentials: true,
|
|
27567
|
+
...options,
|
|
27568
|
+
url: serverUrl(state, options.url),
|
|
27569
|
+
extract: (xhr) => {
|
|
27570
|
+
// Check for 401 Unauthorized
|
|
27571
|
+
if (xhr.status === 401) {
|
|
27572
|
+
// console.log("Received 401 Unauthorized, triggering re-authentication");
|
|
27573
|
+
throw new AuthenticationError();
|
|
27574
|
+
}
|
|
27575
|
+
// Handle empty responses
|
|
27576
|
+
if (!xhr.responseText) {
|
|
27577
|
+
return null;
|
|
27578
|
+
}
|
|
27579
|
+
// Parse JSON response
|
|
27580
|
+
try {
|
|
27581
|
+
return JSON.parse(xhr.responseText);
|
|
27582
|
+
}
|
|
27583
|
+
catch (e) {
|
|
27584
|
+
// If JSON parsing fails, return the raw text
|
|
27585
|
+
return xhr.responseText;
|
|
27586
|
+
}
|
|
27587
|
+
}
|
|
27588
|
+
});
|
|
27589
|
+
}
|
|
27590
|
+
catch (error) {
|
|
27591
|
+
if (error instanceof AuthenticationError && !retries) {
|
|
27592
|
+
// Backend returned 401, perform full authentication and retry (force=true)
|
|
27593
|
+
await ensureAuthenticated(state);
|
|
27594
|
+
// Retry the original request
|
|
27595
|
+
return authenticatedRequest(state, options, retries + 1);
|
|
27596
|
+
}
|
|
27597
|
+
// Re-throw other errors
|
|
27598
|
+
throw error;
|
|
27599
|
+
}
|
|
27600
|
+
};
|
|
27601
|
+
const request = (state, options) => {
|
|
27602
|
+
// Enhanced request with automatic authentication handling
|
|
27603
|
+
return authenticatedRequest(state, options);
|
|
27604
|
+
};
|
|
27605
|
+
const requestMoves = (state, options) => {
|
|
27606
|
+
// Call the moves service on the Google App Engine backend
|
|
27607
|
+
const url = `${state.movesUrl}${options.url}`;
|
|
27608
|
+
const headers = {
|
|
27609
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
27610
|
+
Authorization: `Bearer ${state.movesAccessKey}`,
|
|
27611
|
+
...options === null || options === void 0 ? void 0 : options.headers,
|
|
27612
|
+
};
|
|
27613
|
+
return m.request({
|
|
27614
|
+
withCredentials: false,
|
|
27615
|
+
method: "POST",
|
|
27616
|
+
...options,
|
|
27617
|
+
url,
|
|
27618
|
+
headers,
|
|
27619
|
+
});
|
|
27620
|
+
};
|
|
27621
|
+
|
|
27315
27622
|
const SPINNER_INITIAL_DELAY = 800; // milliseconds
|
|
27316
27623
|
const Spinner = {
|
|
27317
27624
|
// Show a spinner wait box, after an initial delay
|
|
27318
|
-
|
|
27625
|
+
oninit: (vnode) => {
|
|
27319
27626
|
vnode.state.show = false;
|
|
27320
27627
|
vnode.state.ival = setTimeout(() => {
|
|
27321
27628
|
vnode.state.show = true;
|
|
@@ -27428,11 +27735,12 @@ const OnlinePresence = (initialVnode) => {
|
|
|
27428
27735
|
const askServer = attrs.online === undefined;
|
|
27429
27736
|
const id = attrs.id;
|
|
27430
27737
|
const userId = attrs.userId;
|
|
27738
|
+
const state = attrs.state;
|
|
27431
27739
|
let loading = false;
|
|
27432
27740
|
async function _update() {
|
|
27433
27741
|
if (askServer && !loading) {
|
|
27434
27742
|
loading = true;
|
|
27435
|
-
const json = await request({
|
|
27743
|
+
const json = await request(state, {
|
|
27436
27744
|
method: "POST",
|
|
27437
27745
|
url: "/onlinecheck",
|
|
27438
27746
|
body: { user: userId }
|
|
@@ -27442,7 +27750,7 @@ const OnlinePresence = (initialVnode) => {
|
|
|
27442
27750
|
}
|
|
27443
27751
|
}
|
|
27444
27752
|
return {
|
|
27445
|
-
|
|
27753
|
+
oninit: _update,
|
|
27446
27754
|
view: (vnode) => {
|
|
27447
27755
|
var _a, _b;
|
|
27448
27756
|
if (!askServer)
|
|
@@ -27708,6 +28016,7 @@ const WaitDialog = (initialVnode) => {
|
|
|
27708
28016
|
const attrs = initialVnode.attrs;
|
|
27709
28017
|
const view = attrs.view;
|
|
27710
28018
|
const model = view.model;
|
|
28019
|
+
const state = model.state;
|
|
27711
28020
|
const duration = attrs.duration;
|
|
27712
28021
|
const oppId = attrs.oppId;
|
|
27713
28022
|
const key = attrs.challengeKey;
|
|
@@ -27723,9 +28032,9 @@ const WaitDialog = (initialVnode) => {
|
|
|
27723
28032
|
async function updateOnline() {
|
|
27724
28033
|
// Initiate an online check on the opponent
|
|
27725
28034
|
try {
|
|
27726
|
-
if (!oppId || !key)
|
|
28035
|
+
if (!oppId || !key || !state)
|
|
27727
28036
|
return;
|
|
27728
|
-
const json = await request({
|
|
28037
|
+
const json = await request(state, {
|
|
27729
28038
|
method: "POST",
|
|
27730
28039
|
url: "/initwait",
|
|
27731
28040
|
body: { opp: oppId, key }
|
|
@@ -27741,8 +28050,10 @@ const WaitDialog = (initialVnode) => {
|
|
|
27741
28050
|
}
|
|
27742
28051
|
async function cancelWait() {
|
|
27743
28052
|
// Cancel a pending wait for a timed game
|
|
28053
|
+
if (!state)
|
|
28054
|
+
return;
|
|
27744
28055
|
try {
|
|
27745
|
-
await request({
|
|
28056
|
+
await request(state, {
|
|
27746
28057
|
method: "POST",
|
|
27747
28058
|
url: "/cancelwait",
|
|
27748
28059
|
body: {
|
|
@@ -27776,11 +28087,13 @@ const WaitDialog = (initialVnode) => {
|
|
|
27776
28087
|
return {
|
|
27777
28088
|
oncreate,
|
|
27778
28089
|
view: () => {
|
|
28090
|
+
if (!state)
|
|
28091
|
+
return null;
|
|
27779
28092
|
return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
|
|
27780
28093
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
27781
28094
|
m("td", m("h1.chall-icon", glyph("time"))),
|
|
27782
28095
|
m("td.l-border", [
|
|
27783
|
-
m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline }),
|
|
28096
|
+
m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
|
|
27784
28097
|
m("h1", oppNick),
|
|
27785
28098
|
m("h2", oppName)
|
|
27786
28099
|
])
|
|
@@ -27823,6 +28136,7 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27823
28136
|
// is linked up with her opponent and a new game is started
|
|
27824
28137
|
const attrs = initialVnode.attrs;
|
|
27825
28138
|
const view = attrs.view;
|
|
28139
|
+
const state = view.model.state;
|
|
27826
28140
|
const oppId = attrs.oppId;
|
|
27827
28141
|
const key = attrs.challengeKey;
|
|
27828
28142
|
let oppNick = attrs.oppNick;
|
|
@@ -27830,11 +28144,11 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27830
28144
|
let loading = false;
|
|
27831
28145
|
async function waitCheck() {
|
|
27832
28146
|
// Initiate a wait status check on the opponent
|
|
27833
|
-
if (loading)
|
|
28147
|
+
if (loading || !state)
|
|
27834
28148
|
return; // Already checking
|
|
27835
28149
|
loading = true;
|
|
27836
28150
|
try {
|
|
27837
|
-
const json = await request({
|
|
28151
|
+
const json = await request(state, {
|
|
27838
28152
|
method: "POST",
|
|
27839
28153
|
url: "/waitcheck",
|
|
27840
28154
|
body: { user: oppId, key }
|
|
@@ -27857,7 +28171,7 @@ const AcceptDialog = (initialVnode) => {
|
|
|
27857
28171
|
}
|
|
27858
28172
|
}
|
|
27859
28173
|
return {
|
|
27860
|
-
|
|
28174
|
+
oninit: waitCheck,
|
|
27861
28175
|
view: () => {
|
|
27862
28176
|
return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
|
|
27863
28177
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
@@ -28042,87 +28356,6 @@ const FriendCancelConfirmDialog = (initialVnode) => {
|
|
|
28042
28356
|
};
|
|
28043
28357
|
};
|
|
28044
28358
|
|
|
28045
|
-
/*
|
|
28046
|
-
|
|
28047
|
-
Login.ts
|
|
28048
|
-
|
|
28049
|
-
Login UI for Metskrafl using the Mithril library
|
|
28050
|
-
|
|
28051
|
-
Copyright (C) 2025 Miðeind ehf.
|
|
28052
|
-
Author: Vilhjálmur Þorsteinsson
|
|
28053
|
-
|
|
28054
|
-
The Creative Commons Attribution-NonCommercial 4.0
|
|
28055
|
-
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
28056
|
-
For further information, see https://github.com/mideind/Netskrafl
|
|
28057
|
-
|
|
28058
|
-
This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
|
|
28059
|
-
straightforward JavaScript single-page reactive UI library.
|
|
28060
|
-
|
|
28061
|
-
*/
|
|
28062
|
-
const loginUserByEmail = async (email, nickname, fullname, token) => {
|
|
28063
|
-
// Call the /login_malstadur endpoint on the server
|
|
28064
|
-
// to log in the user with the given email and token.
|
|
28065
|
-
// The token is a standard HS256-encoded JWT with aud "netskrafl"
|
|
28066
|
-
// and iss typically "malstadur".
|
|
28067
|
-
return request({
|
|
28068
|
-
method: "POST",
|
|
28069
|
-
url: "/login_malstadur",
|
|
28070
|
-
body: { email, nickname, fullname, token }
|
|
28071
|
-
});
|
|
28072
|
-
};
|
|
28073
|
-
const LoginError = {
|
|
28074
|
-
view: (vnode) => {
|
|
28075
|
-
var _a;
|
|
28076
|
-
return m("div.error", { style: { visibility: "visible" } }, ((_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.message) || "Error logging in");
|
|
28077
|
-
}
|
|
28078
|
-
};
|
|
28079
|
-
const LoginForm = (initialVnode) => {
|
|
28080
|
-
const loginUrl = initialVnode.attrs.loginUrl;
|
|
28081
|
-
let loginInProgress = false;
|
|
28082
|
-
function doLogin(ev) {
|
|
28083
|
-
loginInProgress = true;
|
|
28084
|
-
ev.preventDefault();
|
|
28085
|
-
window.location.href = loginUrl;
|
|
28086
|
-
}
|
|
28087
|
-
return {
|
|
28088
|
-
view: () => {
|
|
28089
|
-
return m.fragment({}, [
|
|
28090
|
-
// This is visible on large screens
|
|
28091
|
-
m("div.loginform-large", [
|
|
28092
|
-
m(NetskraflLogoOnly, {
|
|
28093
|
-
className: "login-logo",
|
|
28094
|
-
width: 200,
|
|
28095
|
-
}),
|
|
28096
|
-
m(NetskraflLegend, {
|
|
28097
|
-
className: "login-legend",
|
|
28098
|
-
width: 600,
|
|
28099
|
-
msStepTime: 0
|
|
28100
|
-
}),
|
|
28101
|
-
mt("div.welcome", "welcome_0"),
|
|
28102
|
-
mt("div.welcome", "welcome_1"),
|
|
28103
|
-
mt("div.welcome", "welcome_2"),
|
|
28104
|
-
m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
|
|
28105
|
-
t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
|
|
28106
|
-
])
|
|
28107
|
-
]),
|
|
28108
|
-
// This is visible on small screens
|
|
28109
|
-
m("div.loginform-small", [
|
|
28110
|
-
m(NetskraflLogoOnly, {
|
|
28111
|
-
className: "login-logo",
|
|
28112
|
-
width: 160,
|
|
28113
|
-
}),
|
|
28114
|
-
m(NetskraflLegend, {
|
|
28115
|
-
className: "login-legend",
|
|
28116
|
-
width: 650,
|
|
28117
|
-
msStepTime: 0
|
|
28118
|
-
}),
|
|
28119
|
-
m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
|
|
28120
|
-
])
|
|
28121
|
-
]);
|
|
28122
|
-
}
|
|
28123
|
-
};
|
|
28124
|
-
};
|
|
28125
|
-
|
|
28126
28359
|
/*
|
|
28127
28360
|
|
|
28128
28361
|
ChallengeDialog.ts
|
|
@@ -28153,7 +28386,11 @@ const ChallengeDialog = () => {
|
|
|
28153
28386
|
m(".chall-hdr", m("table", m("tbody", m("tr", [
|
|
28154
28387
|
m("td", m("h1.chall-icon", glyph("hand-right"))),
|
|
28155
28388
|
m("td.l-border", [
|
|
28156
|
-
m(OnlinePresence, {
|
|
28389
|
+
m(OnlinePresence, {
|
|
28390
|
+
id: "chall-online",
|
|
28391
|
+
userId: item.userid,
|
|
28392
|
+
state,
|
|
28393
|
+
}),
|
|
28157
28394
|
m("h1", item.nick),
|
|
28158
28395
|
m("h2", item.fullname)
|
|
28159
28396
|
])
|
|
@@ -29469,7 +29706,7 @@ const PromoDialog = (initialVnode) => {
|
|
|
29469
29706
|
initFunc();
|
|
29470
29707
|
}
|
|
29471
29708
|
return {
|
|
29472
|
-
|
|
29709
|
+
oninit: _fetchContent,
|
|
29473
29710
|
view: (vnode) => {
|
|
29474
29711
|
let initFunc = vnode.attrs.initFunc;
|
|
29475
29712
|
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", {
|
|
@@ -29541,7 +29778,7 @@ const UserInfoDialog = (initialVnode) => {
|
|
|
29541
29778
|
}
|
|
29542
29779
|
}
|
|
29543
29780
|
return {
|
|
29544
|
-
|
|
29781
|
+
oninit: (vnode) => {
|
|
29545
29782
|
_updateRecentList(vnode);
|
|
29546
29783
|
_updateStats(vnode);
|
|
29547
29784
|
},
|
|
@@ -30531,7 +30768,9 @@ const Board = (initialVnode) => {
|
|
|
30531
30768
|
const scale = view.boardScale || 1.0;
|
|
30532
30769
|
let attrs = {};
|
|
30533
30770
|
// Add handlers for pinch zoom functionality
|
|
30534
|
-
|
|
30771
|
+
// Note: resist the temptation to pass zoomIn/zoomOut directly,
|
|
30772
|
+
// as that would not bind the 'this' pointer correctly
|
|
30773
|
+
addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
|
|
30535
30774
|
if (scale !== 1.0)
|
|
30536
30775
|
attrs.style = `transform: scale(${scale})`;
|
|
30537
30776
|
return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
|
|
@@ -30709,7 +30948,7 @@ const Chat = (initialVnode) => {
|
|
|
30709
30948
|
const { view } = initialVnode.attrs;
|
|
30710
30949
|
const model = view.model;
|
|
30711
30950
|
const game = model.game;
|
|
30712
|
-
model.state;
|
|
30951
|
+
const state = model.state;
|
|
30713
30952
|
function decodeTimestamp(ts) {
|
|
30714
30953
|
// Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
|
|
30715
30954
|
return {
|
|
@@ -30766,7 +31005,7 @@ const Chat = (initialVnode) => {
|
|
|
30766
31005
|
for (const emoticon of EMOTICONS)
|
|
30767
31006
|
if (str.indexOf(emoticon.icon) >= 0) {
|
|
30768
31007
|
// The string contains the emoticon: prepare to replace all occurrences
|
|
30769
|
-
const imgUrl = serverUrl(emoticon.image);
|
|
31008
|
+
const imgUrl = serverUrl(state, emoticon.image);
|
|
30770
31009
|
const img = `<img src='${imgUrl}' height='32' width='32'>`;
|
|
30771
31010
|
// Re the following trick, see https://stackoverflow.com/questions/1144783/
|
|
30772
31011
|
// replacing-all-occurrences-of-a-string-in-javascript
|
|
@@ -31917,13 +32156,13 @@ class View {
|
|
|
31917
32156
|
views.push(this.vwLogin());
|
|
31918
32157
|
break;
|
|
31919
32158
|
case "loginerror":
|
|
31920
|
-
views.push(m(LoginError));
|
|
32159
|
+
views.push(m(LoginError, { key: "login-error" }, t("Villa við innskráningu")));
|
|
31921
32160
|
break;
|
|
31922
32161
|
case "main":
|
|
31923
|
-
views.push(m(Main, { view: this }));
|
|
32162
|
+
views.push(m(Main, { key: "main", view: this }));
|
|
31924
32163
|
break;
|
|
31925
32164
|
case "game":
|
|
31926
|
-
views.push(m(GameView, { view: this }));
|
|
32165
|
+
views.push(m(GameView, { key: "game", view: this }));
|
|
31927
32166
|
break;
|
|
31928
32167
|
case "review":
|
|
31929
32168
|
const n = vwReview(this);
|
|
@@ -31931,7 +32170,7 @@ class View {
|
|
|
31931
32170
|
break;
|
|
31932
32171
|
case "thanks":
|
|
31933
32172
|
// Display a thank-you dialog on top of the normal main screen
|
|
31934
|
-
views.push(m(Main, { view: this }));
|
|
32173
|
+
views.push(m(Main, { key: "main", view: this }));
|
|
31935
32174
|
// Be careful to add the Thanks dialog only once to the stack
|
|
31936
32175
|
if (!this.dialogStack.length)
|
|
31937
32176
|
this.showThanks();
|
|
@@ -31942,7 +32181,7 @@ class View {
|
|
|
31942
32181
|
views.push(this.vwHelp(parseInt(m.route.param("tab") || ""), parseInt(m.route.param("faq") || "")));
|
|
31943
32182
|
break;
|
|
31944
32183
|
default:
|
|
31945
|
-
return [m("div", t("Þessi vefslóð er ekki rétt"))];
|
|
32184
|
+
return [m("div", { key: "error" }, t("Þessi vefslóð er ekki rétt"))];
|
|
31946
32185
|
}
|
|
31947
32186
|
// Push any open dialogs
|
|
31948
32187
|
for (const dialog of this.dialogStack) {
|
|
@@ -31957,7 +32196,7 @@ class View {
|
|
|
31957
32196
|
}
|
|
31958
32197
|
// Overlay a spinner, if active
|
|
31959
32198
|
if (model.spinners)
|
|
31960
|
-
views.push(m(Spinner));
|
|
32199
|
+
views.push(m(Spinner, { key: "spinner" }));
|
|
31961
32200
|
return views;
|
|
31962
32201
|
}
|
|
31963
32202
|
// Dialog support
|
|
@@ -32038,7 +32277,7 @@ class View {
|
|
|
32038
32277
|
zoomOut() {
|
|
32039
32278
|
if (this.boardScale !== 1.0) {
|
|
32040
32279
|
this.boardScale = 1.0;
|
|
32041
|
-
setTimeout(this.resetScale);
|
|
32280
|
+
setTimeout(() => this.resetScale());
|
|
32042
32281
|
}
|
|
32043
32282
|
}
|
|
32044
32283
|
resetScale() {
|
|
@@ -32085,7 +32324,7 @@ class View {
|
|
|
32085
32324
|
// No game or we're in full screen mode: always 100% scale
|
|
32086
32325
|
// Also, as soon as a move is being processed by the server, we zoom out
|
|
32087
32326
|
this.boardScale = 1.0; // Needs to be done before setTimeout() call
|
|
32088
|
-
setTimeout(this.resetScale);
|
|
32327
|
+
setTimeout(() => this.resetScale());
|
|
32089
32328
|
return;
|
|
32090
32329
|
}
|
|
32091
32330
|
const tp = game.tilesPlaced();
|
|
@@ -32179,7 +32418,7 @@ class View {
|
|
|
32179
32418
|
}
|
|
32180
32419
|
}
|
|
32181
32420
|
// Output literal HTML obtained from rawhelp.html on the server
|
|
32182
|
-
return m.fragment({}, [
|
|
32421
|
+
return m.fragment({ key: "help" }, [
|
|
32183
32422
|
m(LeftLogo),
|
|
32184
32423
|
m(UserId, { view: this }),
|
|
32185
32424
|
this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
|
|
@@ -32225,6 +32464,7 @@ class View {
|
|
|
32225
32464
|
vnode.dom.querySelector("#nickname").focus();
|
|
32226
32465
|
}
|
|
32227
32466
|
return m(".modal-dialog", {
|
|
32467
|
+
key: "userprefs",
|
|
32228
32468
|
id: "user-dialog",
|
|
32229
32469
|
oncreate: initFocus
|
|
32230
32470
|
// onupdate: initFocus
|
|
@@ -32306,11 +32546,12 @@ class View {
|
|
|
32306
32546
|
model.loadUser(true); // Activate spinner while loading
|
|
32307
32547
|
if (!model.user)
|
|
32308
32548
|
// Nothing to edit (the spinner should be showing in this case)
|
|
32309
|
-
return m.fragment({}, []);
|
|
32549
|
+
return m.fragment({ key: "userprefs-empty" }, []);
|
|
32310
32550
|
return this.vwUserPrefsDialog();
|
|
32311
32551
|
}
|
|
32312
32552
|
vwUserInfo(args) {
|
|
32313
32553
|
return m(UserInfoDialog, {
|
|
32554
|
+
key: "userinfodialog-" + args.userid,
|
|
32314
32555
|
view: this,
|
|
32315
32556
|
userid: args.userid,
|
|
32316
32557
|
nick: args.nick,
|
|
@@ -32319,6 +32560,7 @@ class View {
|
|
|
32319
32560
|
}
|
|
32320
32561
|
vwPromo(args) {
|
|
32321
32562
|
return m(PromoDialog, {
|
|
32563
|
+
key: "promo-" + args.kind,
|
|
32322
32564
|
view: this,
|
|
32323
32565
|
kind: args.kind,
|
|
32324
32566
|
initFunc: args.initFunc
|
|
@@ -32326,6 +32568,7 @@ class View {
|
|
|
32326
32568
|
}
|
|
32327
32569
|
vwWait(args) {
|
|
32328
32570
|
return m(WaitDialog, {
|
|
32571
|
+
key: "wait-" + args.challengeKey,
|
|
32329
32572
|
view: this,
|
|
32330
32573
|
oppId: args.oppId,
|
|
32331
32574
|
oppNick: args.oppNick,
|
|
@@ -32336,6 +32579,7 @@ class View {
|
|
|
32336
32579
|
}
|
|
32337
32580
|
vwAccept(args) {
|
|
32338
32581
|
return m(AcceptDialog, {
|
|
32582
|
+
key: "accept-" + args.challengeKey,
|
|
32339
32583
|
view: this,
|
|
32340
32584
|
oppId: args.oppId,
|
|
32341
32585
|
oppNick: args.oppNick,
|
|
@@ -32346,7 +32590,7 @@ class View {
|
|
|
32346
32590
|
var _a;
|
|
32347
32591
|
const model = this.model;
|
|
32348
32592
|
const loginUrl = ((_a = model.state) === null || _a === void 0 ? void 0 : _a.loginUrl) || "";
|
|
32349
|
-
return m(LoginForm, { loginUrl });
|
|
32593
|
+
return m(LoginForm, { key: "login", loginUrl });
|
|
32350
32594
|
}
|
|
32351
32595
|
vwDialogs() {
|
|
32352
32596
|
// Show prompt dialogs below game board, if any
|
|
@@ -32425,12 +32669,12 @@ class View {
|
|
|
32425
32669
|
View.dialogViews = {
|
|
32426
32670
|
userprefs: (view) => view.vwUserPrefs(),
|
|
32427
32671
|
userinfo: (view, args) => view.vwUserInfo(args),
|
|
32428
|
-
challenge: (view, args) => m(ChallengeDialog, { view, item: args }),
|
|
32672
|
+
challenge: (view, args) => m(ChallengeDialog, { key: "challenge-" + args.item.challenge_key, view, item: args }),
|
|
32429
32673
|
promo: (view, args) => view.vwPromo(args),
|
|
32430
|
-
friend: (view) => m(FriendPromoteDialog, { view }),
|
|
32431
|
-
thanks: (view) => m(FriendThanksDialog, { view }),
|
|
32432
|
-
cancel: (view) => m(FriendCancelDialog, { view }),
|
|
32433
|
-
confirm: (view) => m(FriendCancelConfirmDialog, { view }),
|
|
32674
|
+
friend: (view) => m(FriendPromoteDialog, { key: "friend", view }),
|
|
32675
|
+
thanks: (view) => m(FriendThanksDialog, { key: "thanks", view }),
|
|
32676
|
+
cancel: (view) => m(FriendCancelDialog, { key: "cancel", view }),
|
|
32677
|
+
confirm: (view) => m(FriendCancelConfirmDialog, { key: "confirm", view }),
|
|
32434
32678
|
wait: (view, args) => view.vwWait(args),
|
|
32435
32679
|
accept: (view, args) => view.vwAccept(args)
|
|
32436
32680
|
};
|
|
@@ -32468,7 +32712,7 @@ class WordChecker {
|
|
|
32468
32712
|
}
|
|
32469
32713
|
}
|
|
32470
32714
|
}
|
|
32471
|
-
async checkWords(locale, words) {
|
|
32715
|
+
async checkWords(state, locale, words) {
|
|
32472
32716
|
// Return true if all words are valid in the given locale,
|
|
32473
32717
|
// or false otherwise. Lookups are cached for efficiency.
|
|
32474
32718
|
let cache = this.wordCheckCache[locale];
|
|
@@ -32496,7 +32740,7 @@ class WordChecker {
|
|
|
32496
32740
|
}
|
|
32497
32741
|
// We need a server roundtrip
|
|
32498
32742
|
try {
|
|
32499
|
-
const response = await requestMoves({
|
|
32743
|
+
const response = await requestMoves(state, {
|
|
32500
32744
|
url: "/wordcheck",
|
|
32501
32745
|
body: {
|
|
32502
32746
|
locale,
|
|
@@ -32726,7 +32970,7 @@ const BOARD = {
|
|
|
32726
32970
|
}
|
|
32727
32971
|
};
|
|
32728
32972
|
class BaseGame {
|
|
32729
|
-
constructor(uuid, board_type = "standard") {
|
|
32973
|
+
constructor(uuid, state, board_type = "standard") {
|
|
32730
32974
|
// Basic game properties that don't change while the game is underway
|
|
32731
32975
|
this.locale = "is_IS";
|
|
32732
32976
|
this.alphabet = "";
|
|
@@ -32760,6 +33004,7 @@ class BaseGame {
|
|
|
32760
33004
|
this.board_type = board_type;
|
|
32761
33005
|
this.startSquare = START_SQUARE[board_type];
|
|
32762
33006
|
this.startCoord = START_COORD[board_type];
|
|
33007
|
+
this.state = state;
|
|
32763
33008
|
}
|
|
32764
33009
|
// Default init method that can be overridden
|
|
32765
33010
|
init() {
|
|
@@ -33000,7 +33245,7 @@ class BaseGame {
|
|
|
33000
33245
|
if (!this.manual) {
|
|
33001
33246
|
// This is not a manual-wordcheck game:
|
|
33002
33247
|
// Check the word that has been laid down
|
|
33003
|
-
const found = await wordChecker.checkWords(this.locale, scoreResult.words);
|
|
33248
|
+
const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
|
|
33004
33249
|
this.wordGood = found;
|
|
33005
33250
|
this.wordBad = !found;
|
|
33006
33251
|
}
|
|
@@ -33263,10 +33508,10 @@ const MAX_OVERTIME = 10 * 60.0;
|
|
|
33263
33508
|
const DEBUG_OVERTIME = 1 * 60.0;
|
|
33264
33509
|
const GAME_OVER = 99; // Error code corresponding to the Error class in skraflmechanics.py
|
|
33265
33510
|
class Game extends BaseGame {
|
|
33266
|
-
constructor(uuid, srvGame, moveListener, maxOvertime) {
|
|
33511
|
+
constructor(uuid, srvGame, moveListener, state, maxOvertime) {
|
|
33267
33512
|
var _a;
|
|
33268
33513
|
// Call parent constructor
|
|
33269
|
-
super(uuid); // Default board_type: "standard"
|
|
33514
|
+
super(uuid, state); // Default board_type: "standard"
|
|
33270
33515
|
// A class that represents a Netskrafl game instance on the client
|
|
33271
33516
|
// Netskrafl-specific properties
|
|
33272
33517
|
this.userid = ["", ""];
|
|
@@ -33523,7 +33768,7 @@ class Game extends BaseGame {
|
|
|
33523
33768
|
try {
|
|
33524
33769
|
if (!this.uuid)
|
|
33525
33770
|
return;
|
|
33526
|
-
const result = await request({
|
|
33771
|
+
const result = await request(this.state, {
|
|
33527
33772
|
method: "POST",
|
|
33528
33773
|
url: "/gamestate",
|
|
33529
33774
|
body: { game: this.uuid } // !!! FIXME: Add delete_zombie parameter
|
|
@@ -33726,7 +33971,7 @@ class Game extends BaseGame {
|
|
|
33726
33971
|
this.chatLoading = true;
|
|
33727
33972
|
this.messages = [];
|
|
33728
33973
|
try {
|
|
33729
|
-
const result = await request({
|
|
33974
|
+
const result = await request(this.state, {
|
|
33730
33975
|
method: "POST",
|
|
33731
33976
|
url: "/chatload",
|
|
33732
33977
|
body: { channel: "game:" + this.uuid }
|
|
@@ -33752,7 +33997,7 @@ class Game extends BaseGame {
|
|
|
33752
33997
|
// Load statistics about a game
|
|
33753
33998
|
this.stats = undefined; // Error/in-progress status
|
|
33754
33999
|
try {
|
|
33755
|
-
const json = await request({
|
|
34000
|
+
const json = await request(this.state, {
|
|
33756
34001
|
method: "POST",
|
|
33757
34002
|
url: "/gamestats",
|
|
33758
34003
|
body: { game: this.uuid }
|
|
@@ -33772,7 +34017,7 @@ class Game extends BaseGame {
|
|
|
33772
34017
|
async sendMessage(msg) {
|
|
33773
34018
|
// Send a chat message
|
|
33774
34019
|
try {
|
|
33775
|
-
await request({
|
|
34020
|
+
await request(this.state, {
|
|
33776
34021
|
method: "POST",
|
|
33777
34022
|
url: "/chatmsg",
|
|
33778
34023
|
body: { channel: "game:" + this.uuid, msg: msg }
|
|
@@ -33974,10 +34219,10 @@ class Game extends BaseGame {
|
|
|
33974
34219
|
// Send a move to the server
|
|
33975
34220
|
this.moveInProgress = true;
|
|
33976
34221
|
try {
|
|
33977
|
-
const result = await request({
|
|
34222
|
+
const result = await request(this.state, {
|
|
33978
34223
|
method: "POST",
|
|
33979
34224
|
url: "/submitmove",
|
|
33980
|
-
body: { moves: moves, mcount: this.moves.length, uuid: this.uuid }
|
|
34225
|
+
body: { moves: moves, mcount: this.moves.length, uuid: this.uuid },
|
|
33981
34226
|
});
|
|
33982
34227
|
// The update() function also handles error results
|
|
33983
34228
|
this.update(result);
|
|
@@ -34003,7 +34248,7 @@ class Game extends BaseGame {
|
|
|
34003
34248
|
// Force resignation by a tardy opponent
|
|
34004
34249
|
this.moveInProgress = true;
|
|
34005
34250
|
try {
|
|
34006
|
-
const result = await request({
|
|
34251
|
+
const result = await request(this.state, {
|
|
34007
34252
|
method: "POST",
|
|
34008
34253
|
url: "/forceresign",
|
|
34009
34254
|
body: { mcount: this.moves.length, game: this.uuid }
|
|
@@ -34127,7 +34372,10 @@ const HOT_WARM_BOUNDARY_RATIO = 0.6;
|
|
|
34127
34372
|
const WARM_COLD_BOUNDARY_RATIO = 0.3;
|
|
34128
34373
|
class Riddle extends BaseGame {
|
|
34129
34374
|
constructor(uuid, date, model) {
|
|
34130
|
-
|
|
34375
|
+
if (!model.state) {
|
|
34376
|
+
throw new Error("No global state in Riddle constructor");
|
|
34377
|
+
}
|
|
34378
|
+
super(uuid, model.state);
|
|
34131
34379
|
// Scoring properties, static
|
|
34132
34380
|
this.bestPossibleScore = 0;
|
|
34133
34381
|
this.warmBoundary = 0;
|
|
@@ -34168,9 +34416,12 @@ class Riddle extends BaseGame {
|
|
|
34168
34416
|
async load(date, locale) {
|
|
34169
34417
|
this.date = date;
|
|
34170
34418
|
this.locale = locale;
|
|
34419
|
+
const { state } = this;
|
|
34171
34420
|
try {
|
|
34421
|
+
if (!state)
|
|
34422
|
+
throw new Error("No global state in Riddle.load");
|
|
34172
34423
|
// Request riddle data from server (HTTP API call)
|
|
34173
|
-
const response = await request({
|
|
34424
|
+
const response = await request(state, {
|
|
34174
34425
|
method: "POST",
|
|
34175
34426
|
url: "/gatadagsins/riddle",
|
|
34176
34427
|
body: { date, locale }
|
|
@@ -34198,11 +34449,11 @@ class Riddle extends BaseGame {
|
|
|
34198
34449
|
}
|
|
34199
34450
|
}
|
|
34200
34451
|
async submitRiddleWord(move) {
|
|
34201
|
-
const { state } = this
|
|
34452
|
+
const { state } = this;
|
|
34202
34453
|
if (!state || !state.userId)
|
|
34203
34454
|
return;
|
|
34204
34455
|
try {
|
|
34205
|
-
await request({
|
|
34456
|
+
await request(state, {
|
|
34206
34457
|
method: "POST",
|
|
34207
34458
|
url: "/gatadagsins/submit",
|
|
34208
34459
|
body: {
|
|
@@ -34482,8 +34733,6 @@ function getSettings() {
|
|
|
34482
34733
|
}
|
|
34483
34734
|
class Model {
|
|
34484
34735
|
constructor(settings, state) {
|
|
34485
|
-
// A class for the underlying data model, displayed by the current view
|
|
34486
|
-
this.state = null;
|
|
34487
34736
|
this.paths = [];
|
|
34488
34737
|
// The routeName will be "login", "main", "game"...
|
|
34489
34738
|
this.routeName = undefined;
|
|
@@ -34542,7 +34791,54 @@ class Model {
|
|
|
34542
34791
|
this.isExplo = state.isExplo;
|
|
34543
34792
|
this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
|
|
34544
34793
|
// Load localized text messages from the messages.json file
|
|
34545
|
-
loadMessages(state.locale);
|
|
34794
|
+
loadMessages(state, state.locale);
|
|
34795
|
+
}
|
|
34796
|
+
// Simple POST request with JSON body (most common case)
|
|
34797
|
+
async post(url, body) {
|
|
34798
|
+
if (!this.state) {
|
|
34799
|
+
throw new Error("Model state is not initialized");
|
|
34800
|
+
}
|
|
34801
|
+
return request(this.state, {
|
|
34802
|
+
method: "POST",
|
|
34803
|
+
url,
|
|
34804
|
+
body
|
|
34805
|
+
});
|
|
34806
|
+
}
|
|
34807
|
+
// GET request for HTML/text content
|
|
34808
|
+
async getText(url) {
|
|
34809
|
+
if (!this.state) {
|
|
34810
|
+
throw new Error("Model state is not initialized");
|
|
34811
|
+
}
|
|
34812
|
+
return request(this.state, {
|
|
34813
|
+
method: "GET",
|
|
34814
|
+
url,
|
|
34815
|
+
responseType: "text",
|
|
34816
|
+
deserialize: (str) => str
|
|
34817
|
+
});
|
|
34818
|
+
}
|
|
34819
|
+
// POST request that returns text/HTML
|
|
34820
|
+
async postText(url, body) {
|
|
34821
|
+
if (!this.state) {
|
|
34822
|
+
throw new Error("Model state is not initialized");
|
|
34823
|
+
}
|
|
34824
|
+
return request(this.state, {
|
|
34825
|
+
method: "POST",
|
|
34826
|
+
url,
|
|
34827
|
+
body,
|
|
34828
|
+
responseType: "text",
|
|
34829
|
+
deserialize: (str) => str
|
|
34830
|
+
});
|
|
34831
|
+
}
|
|
34832
|
+
// Request to the moves service
|
|
34833
|
+
async postMoves(body) {
|
|
34834
|
+
if (!this.state) {
|
|
34835
|
+
throw new Error("Model state is not initialized");
|
|
34836
|
+
}
|
|
34837
|
+
return requestMoves(this.state, {
|
|
34838
|
+
method: "POST",
|
|
34839
|
+
url: "/moves",
|
|
34840
|
+
body
|
|
34841
|
+
});
|
|
34546
34842
|
}
|
|
34547
34843
|
async loadGame(uuid, funcComplete, deleteZombie = false) {
|
|
34548
34844
|
var _a;
|
|
@@ -34561,20 +34857,16 @@ class Model {
|
|
|
34561
34857
|
this.highlightedMove = null;
|
|
34562
34858
|
if (!uuid)
|
|
34563
34859
|
return; // Should not happen
|
|
34564
|
-
const result = await
|
|
34565
|
-
|
|
34566
|
-
|
|
34567
|
-
body: {
|
|
34568
|
-
game: uuid,
|
|
34569
|
-
delete_zombie: deleteZombie
|
|
34570
|
-
}
|
|
34860
|
+
const result = await this.post("/gamestate", {
|
|
34861
|
+
game: uuid,
|
|
34862
|
+
delete_zombie: deleteZombie
|
|
34571
34863
|
});
|
|
34572
34864
|
if (!(result === null || result === void 0 ? void 0 : result.ok)) {
|
|
34573
34865
|
// console.log("Game " + uuid + " could not be loaded");
|
|
34574
34866
|
}
|
|
34575
34867
|
else {
|
|
34576
34868
|
// Create a new game instance and load the state into it
|
|
34577
|
-
this.game = new Game(uuid, result.game, this, ((_a = this.state) === null || _a === void 0 ? void 0 : _a.runningLocal) ? DEBUG_OVERTIME : MAX_OVERTIME);
|
|
34869
|
+
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);
|
|
34578
34870
|
// Successfully loaded: call the completion function, if given
|
|
34579
34871
|
// (this usually attaches the Firebase event listener)
|
|
34580
34872
|
funcComplete && funcComplete();
|
|
@@ -34597,11 +34889,7 @@ class Model {
|
|
|
34597
34889
|
this.numChallenges = 0;
|
|
34598
34890
|
this.oppReady = 0;
|
|
34599
34891
|
try {
|
|
34600
|
-
const json = await
|
|
34601
|
-
method: "POST",
|
|
34602
|
-
url: "/allgamelists",
|
|
34603
|
-
body: { zombie: includeZombies, count: 40 }
|
|
34604
|
-
});
|
|
34892
|
+
const json = await this.post("/allgamelists", { zombie: includeZombies, count: 40 });
|
|
34605
34893
|
if (!json || json.result !== 0) {
|
|
34606
34894
|
// An error occurred
|
|
34607
34895
|
this.gameList = [];
|
|
@@ -34643,11 +34931,7 @@ class Model {
|
|
|
34643
34931
|
this.numGames = 0;
|
|
34644
34932
|
this.spinners++;
|
|
34645
34933
|
try {
|
|
34646
|
-
const json = await
|
|
34647
|
-
method: "POST",
|
|
34648
|
-
url: "/gamelist",
|
|
34649
|
-
body: { zombie: includeZombies }
|
|
34650
|
-
});
|
|
34934
|
+
const json = await this.post("/gamelist", { zombie: includeZombies });
|
|
34651
34935
|
if (!json || json.result !== 0) {
|
|
34652
34936
|
// An error occurred
|
|
34653
34937
|
this.gameList = [];
|
|
@@ -34677,10 +34961,7 @@ class Model {
|
|
|
34677
34961
|
this.oppReady = 0;
|
|
34678
34962
|
this.spinners++; // Show spinner while loading
|
|
34679
34963
|
try {
|
|
34680
|
-
const json = await
|
|
34681
|
-
method: "POST",
|
|
34682
|
-
url: "/challengelist"
|
|
34683
|
-
});
|
|
34964
|
+
const json = await this.post("/challengelist");
|
|
34684
34965
|
if (!json || json.result !== 0) {
|
|
34685
34966
|
// An error occurred
|
|
34686
34967
|
this.challengeList = [];
|
|
@@ -34714,11 +34995,7 @@ class Model {
|
|
|
34714
34995
|
this.recentList = [];
|
|
34715
34996
|
this.spinners++; // Show spinner while loading
|
|
34716
34997
|
try {
|
|
34717
|
-
const json = await
|
|
34718
|
-
method: "POST",
|
|
34719
|
-
url: "/recentlist",
|
|
34720
|
-
body: { versus: null, count: 40 }
|
|
34721
|
-
});
|
|
34998
|
+
const json = await this.post("/recentlist", { versus: null, count: 40 });
|
|
34722
34999
|
if (!json || json.result !== 0) {
|
|
34723
35000
|
// An error occurred
|
|
34724
35001
|
this.recentList = [];
|
|
@@ -34737,11 +35014,7 @@ class Model {
|
|
|
34737
35014
|
}
|
|
34738
35015
|
async loadUserRecentList(userid, versus, readyFunc) {
|
|
34739
35016
|
// Load the list of recent games for the given user
|
|
34740
|
-
const json = await
|
|
34741
|
-
method: "POST",
|
|
34742
|
-
url: "/recentlist",
|
|
34743
|
-
body: { user: userid, versus: versus, count: 40 }
|
|
34744
|
-
});
|
|
35017
|
+
const json = await this.post("/recentlist", { user: userid, versus: versus, count: 40 });
|
|
34745
35018
|
readyFunc(json);
|
|
34746
35019
|
}
|
|
34747
35020
|
async loadUserList(criteria) {
|
|
@@ -34759,11 +35032,7 @@ class Model {
|
|
|
34759
35032
|
const url = "/userlist";
|
|
34760
35033
|
const body = criteria;
|
|
34761
35034
|
try {
|
|
34762
|
-
const json = await
|
|
34763
|
-
method: "POST",
|
|
34764
|
-
url,
|
|
34765
|
-
body,
|
|
34766
|
-
});
|
|
35035
|
+
const json = await this.post(url, body);
|
|
34767
35036
|
if (!json || json.result !== 0) {
|
|
34768
35037
|
// An error occurred
|
|
34769
35038
|
this.userList = [];
|
|
@@ -34785,11 +35054,7 @@ class Model {
|
|
|
34785
35054
|
const url = "/rating";
|
|
34786
35055
|
const body = { kind: spec };
|
|
34787
35056
|
try {
|
|
34788
|
-
const json = await
|
|
34789
|
-
method: "POST",
|
|
34790
|
-
url,
|
|
34791
|
-
body,
|
|
34792
|
-
});
|
|
35057
|
+
const json = await this.post(url, body);
|
|
34793
35058
|
if (!json || json.result !== 0) {
|
|
34794
35059
|
// An error occurred
|
|
34795
35060
|
this.eloRatingList = [];
|
|
@@ -34808,11 +35073,7 @@ class Model {
|
|
|
34808
35073
|
// Load statistics for the current user
|
|
34809
35074
|
this.ownStats = {};
|
|
34810
35075
|
try {
|
|
34811
|
-
const json = await
|
|
34812
|
-
method: "POST",
|
|
34813
|
-
url: "/userstats",
|
|
34814
|
-
body: {} // Current user is implicit
|
|
34815
|
-
});
|
|
35076
|
+
const json = await this.post("/userstats", {});
|
|
34816
35077
|
if (!json || json.result !== 0) {
|
|
34817
35078
|
// An error occurred
|
|
34818
35079
|
return;
|
|
@@ -34825,11 +35086,7 @@ class Model {
|
|
|
34825
35086
|
async loadUserStats(userid, readyFunc) {
|
|
34826
35087
|
// Load statistics for the given user
|
|
34827
35088
|
try {
|
|
34828
|
-
const json = await
|
|
34829
|
-
method: "POST",
|
|
34830
|
-
url: "/userstats",
|
|
34831
|
-
body: { user: userid }
|
|
34832
|
-
});
|
|
35089
|
+
const json = await this.post("/userstats", { user: userid });
|
|
34833
35090
|
readyFunc(json);
|
|
34834
35091
|
}
|
|
34835
35092
|
catch (e) {
|
|
@@ -34839,13 +35096,7 @@ class Model {
|
|
|
34839
35096
|
async loadPromoContent(key, readyFunc) {
|
|
34840
35097
|
// Load HTML content for promo dialog
|
|
34841
35098
|
try {
|
|
34842
|
-
const html = await
|
|
34843
|
-
method: "POST",
|
|
34844
|
-
url: "/promo",
|
|
34845
|
-
body: { key: key },
|
|
34846
|
-
responseType: "text",
|
|
34847
|
-
deserialize: (str) => str
|
|
34848
|
-
});
|
|
35099
|
+
const html = await this.postText("/promo", { key: key });
|
|
34849
35100
|
readyFunc(html);
|
|
34850
35101
|
}
|
|
34851
35102
|
catch (e) {
|
|
@@ -34894,11 +35145,7 @@ class Model {
|
|
|
34894
35145
|
rack,
|
|
34895
35146
|
limit: NUM_BEST_MOVES,
|
|
34896
35147
|
};
|
|
34897
|
-
const json = await
|
|
34898
|
-
method: "POST",
|
|
34899
|
-
url: "/moves",
|
|
34900
|
-
body: rq,
|
|
34901
|
-
});
|
|
35148
|
+
const json = await this.postMoves(rq);
|
|
34902
35149
|
this.highlightedMove = null;
|
|
34903
35150
|
if (!json || json.moves === undefined) {
|
|
34904
35151
|
// Something unexpected going on
|
|
@@ -34933,12 +35180,7 @@ class Model {
|
|
|
34933
35180
|
return; // Already loaded
|
|
34934
35181
|
try {
|
|
34935
35182
|
const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
34936
|
-
const result = await
|
|
34937
|
-
method: "GET",
|
|
34938
|
-
url: "/rawhelp?version=malstadur&locale=" + locale,
|
|
34939
|
-
responseType: "text",
|
|
34940
|
-
deserialize: (str) => str
|
|
34941
|
-
});
|
|
35183
|
+
const result = await this.getText("/rawhelp?version=malstadur&locale=" + locale);
|
|
34942
35184
|
this.helpHTML = result;
|
|
34943
35185
|
}
|
|
34944
35186
|
catch (e) {
|
|
@@ -34953,12 +35195,7 @@ class Model {
|
|
|
34953
35195
|
return; // Already loaded
|
|
34954
35196
|
try {
|
|
34955
35197
|
const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
34956
|
-
const result = await
|
|
34957
|
-
method: "GET",
|
|
34958
|
-
url: "/friend?locale=" + locale,
|
|
34959
|
-
responseType: "text",
|
|
34960
|
-
deserialize: (str) => str
|
|
34961
|
-
});
|
|
35198
|
+
const result = await this.getText("/friend?locale=" + locale);
|
|
34962
35199
|
this.friendHTML = result;
|
|
34963
35200
|
}
|
|
34964
35201
|
catch (e) {
|
|
@@ -34974,10 +35211,7 @@ class Model {
|
|
|
34974
35211
|
this.spinners++;
|
|
34975
35212
|
}
|
|
34976
35213
|
try {
|
|
34977
|
-
const result = await
|
|
34978
|
-
method: "POST",
|
|
34979
|
-
url: "/loaduserprefs",
|
|
34980
|
-
});
|
|
35214
|
+
const result = await this.post("/loaduserprefs");
|
|
34981
35215
|
if (!result || !result.ok) {
|
|
34982
35216
|
this.user = null;
|
|
34983
35217
|
this.userErrors = null;
|
|
@@ -35004,11 +35238,7 @@ class Model {
|
|
|
35004
35238
|
if (!user)
|
|
35005
35239
|
return;
|
|
35006
35240
|
try {
|
|
35007
|
-
const result = await
|
|
35008
|
-
method: "POST",
|
|
35009
|
-
url: "/saveuserprefs",
|
|
35010
|
-
body: user
|
|
35011
|
-
});
|
|
35241
|
+
const result = await this.post("/saveuserprefs", user);
|
|
35012
35242
|
if (result === null || result === void 0 ? void 0 : result.ok) {
|
|
35013
35243
|
// User preferences modified successfully on the server:
|
|
35014
35244
|
// update the state variables that we're caching
|
|
@@ -35017,6 +35247,7 @@ class Model {
|
|
|
35017
35247
|
state.userNick = user.nickname;
|
|
35018
35248
|
state.beginner = user.beginner;
|
|
35019
35249
|
state.fairPlay = user.fairplay;
|
|
35250
|
+
saveAuthSettings(state);
|
|
35020
35251
|
}
|
|
35021
35252
|
// Note that state.plan is updated via a Firebase notification
|
|
35022
35253
|
// Give the game instance a chance to update its state
|
|
@@ -35051,41 +35282,22 @@ class Model {
|
|
|
35051
35282
|
return false;
|
|
35052
35283
|
}
|
|
35053
35284
|
handleUserMessage(json, firstAttach) {
|
|
35054
|
-
var _a;
|
|
35055
35285
|
// Handle an incoming Firebase user message, i.e. a message
|
|
35056
35286
|
// on the /user/[userid] path
|
|
35057
|
-
if (firstAttach || !this.state)
|
|
35287
|
+
if (firstAttach || !this.state || !json)
|
|
35058
35288
|
return;
|
|
35059
35289
|
let redraw = false;
|
|
35060
|
-
if (json.
|
|
35061
|
-
// Potential change of user friendship status
|
|
35062
|
-
const newFriend = json.friend ? true : false;
|
|
35063
|
-
if (this.user && this.user.friend != newFriend) {
|
|
35064
|
-
this.user.friend = newFriend;
|
|
35065
|
-
redraw = true;
|
|
35066
|
-
}
|
|
35067
|
-
}
|
|
35068
|
-
if (json.plan !== undefined) {
|
|
35290
|
+
if (typeof json.plan === "string") {
|
|
35069
35291
|
// Potential change of user subscription plan
|
|
35070
|
-
if (this.state.plan
|
|
35292
|
+
if (this.state.plan !== json.plan) {
|
|
35071
35293
|
this.state.plan = json.plan;
|
|
35072
35294
|
redraw = true;
|
|
35073
35295
|
}
|
|
35074
|
-
if (this.user && !this.user.friend && this.state.plan == "friend") {
|
|
35075
|
-
// plan == "friend" implies that user.friend should be true
|
|
35076
|
-
this.user.friend = true;
|
|
35077
|
-
redraw = true;
|
|
35078
|
-
}
|
|
35079
|
-
if (this.state.plan == "" && ((_a = this.user) === null || _a === void 0 ? void 0 : _a.friend)) {
|
|
35080
|
-
// Conversely, an empty plan string means that the user is not a friend
|
|
35081
|
-
this.user.friend = false;
|
|
35082
|
-
redraw = true;
|
|
35083
|
-
}
|
|
35084
35296
|
}
|
|
35085
35297
|
if (json.hasPaid !== undefined) {
|
|
35086
35298
|
// Potential change of payment status
|
|
35087
|
-
const newHasPaid = (this.state.plan
|
|
35088
|
-
if (this.state.hasPaid
|
|
35299
|
+
const newHasPaid = (this.state.plan !== "" && json.hasPaid) ? true : false;
|
|
35300
|
+
if (this.state.hasPaid !== newHasPaid) {
|
|
35089
35301
|
this.state.hasPaid = newHasPaid;
|
|
35090
35302
|
redraw = true;
|
|
35091
35303
|
}
|
|
@@ -35422,8 +35634,10 @@ class Actions {
|
|
|
35422
35634
|
}
|
|
35423
35635
|
async markFavorite(userId, status) {
|
|
35424
35636
|
// Mark or de-mark a user as a favorite
|
|
35637
|
+
if (!this.model.state)
|
|
35638
|
+
return;
|
|
35425
35639
|
try {
|
|
35426
|
-
await request({
|
|
35640
|
+
await request(this.model.state, {
|
|
35427
35641
|
method: "POST",
|
|
35428
35642
|
url: "/favorite",
|
|
35429
35643
|
body: { destuser: userId, action: status ? "add" : "delete" }
|
|
@@ -35441,8 +35655,10 @@ class Actions {
|
|
|
35441
35655
|
async handleChallenge(parameters) {
|
|
35442
35656
|
var _a;
|
|
35443
35657
|
// Reject or retract a challenge
|
|
35658
|
+
if (!this.model.state)
|
|
35659
|
+
return;
|
|
35444
35660
|
try {
|
|
35445
|
-
const json = await request({
|
|
35661
|
+
const json = await request(this.model.state, {
|
|
35446
35662
|
method: "POST",
|
|
35447
35663
|
url: "/challenge",
|
|
35448
35664
|
body: parameters
|
|
@@ -35497,6 +35713,8 @@ class Actions {
|
|
|
35497
35713
|
async startNewGame(oppid, reverse = false) {
|
|
35498
35714
|
var _a;
|
|
35499
35715
|
// Ask the server to initiate a new game against the given opponent
|
|
35716
|
+
if (!this.model.state)
|
|
35717
|
+
return;
|
|
35500
35718
|
try {
|
|
35501
35719
|
const rqBody = { opp: oppid, rev: reverse };
|
|
35502
35720
|
if (this.model.isExplo) {
|
|
@@ -35509,7 +35727,7 @@ class Actions {
|
|
|
35509
35727
|
url: "/initgame",
|
|
35510
35728
|
body: rqBody
|
|
35511
35729
|
};
|
|
35512
|
-
const json = await request(rq);
|
|
35730
|
+
const json = await request(this.model.state, rq);
|
|
35513
35731
|
if (json === null || json === void 0 ? void 0 : json.ok) {
|
|
35514
35732
|
// Log the new game event
|
|
35515
35733
|
const locale = ((_a = this.model.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
|
|
@@ -35575,12 +35793,16 @@ class Actions {
|
|
|
35575
35793
|
// User Preference Management Actions
|
|
35576
35794
|
async setUserPref(pref) {
|
|
35577
35795
|
// Set a user preference on the server
|
|
35796
|
+
if (!this.model.state)
|
|
35797
|
+
return;
|
|
35578
35798
|
try {
|
|
35579
|
-
await request({
|
|
35799
|
+
await request(this.model.state, {
|
|
35580
35800
|
method: "POST",
|
|
35581
35801
|
url: "/setuserpref",
|
|
35582
35802
|
body: pref
|
|
35583
35803
|
}); // No result required or expected
|
|
35804
|
+
// Update the persisted settings in sessionStorage
|
|
35805
|
+
saveAuthSettings(this.model.state);
|
|
35584
35806
|
}
|
|
35585
35807
|
catch (e) {
|
|
35586
35808
|
// A future TODO might be to signal an error in the UI
|
|
@@ -35593,7 +35815,7 @@ class Actions {
|
|
|
35593
35815
|
if (!user || !state)
|
|
35594
35816
|
return false;
|
|
35595
35817
|
try {
|
|
35596
|
-
const json = await request({
|
|
35818
|
+
const json = await request(state, {
|
|
35597
35819
|
method: "POST",
|
|
35598
35820
|
url: "/cancelplan",
|
|
35599
35821
|
body: {}
|
|
@@ -35711,41 +35933,25 @@ async function main$1(state, container) {
|
|
|
35711
35933
|
console.error("No container element found");
|
|
35712
35934
|
return "error";
|
|
35713
35935
|
}
|
|
35714
|
-
// Set up Netskrafl backend server URLs
|
|
35715
|
-
setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
|
|
35716
35936
|
try {
|
|
35717
|
-
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35721
|
-
|
|
35722
|
-
|
|
35723
|
-
|
|
35724
|
-
|
|
35725
|
-
|
|
35726
|
-
state.userNick = loginData.nickname || state.userNick;
|
|
35727
|
-
// Log in to Firebase with the token passed from the server
|
|
35728
|
-
await loginFirebase(state, loginData.firebase_token);
|
|
35729
|
-
// Everything looks OK:
|
|
35730
|
-
// Create the model, actions and view objects in proper sequence
|
|
35731
|
-
const settings = getSettings();
|
|
35732
|
-
const model = new Model(settings, state);
|
|
35733
|
-
const actions = new Actions(model);
|
|
35734
|
-
const view = new View(actions);
|
|
35735
|
-
// Run the Mithril router
|
|
35736
|
-
const routeResolver = createRouteResolver(actions, view);
|
|
35737
|
-
m.route(container, settings.defaultRoute, routeResolver);
|
|
35738
|
-
return "success";
|
|
35739
|
-
}
|
|
35937
|
+
// Skip initial login - authentication will happen lazily on first API call
|
|
35938
|
+
// Create the model, actions and view objects in proper sequence
|
|
35939
|
+
const settings = getSettings();
|
|
35940
|
+
const model = new Model(settings, state);
|
|
35941
|
+
const actions = new Actions(model);
|
|
35942
|
+
const view = new View(actions);
|
|
35943
|
+
// Run the Mithril router
|
|
35944
|
+
const routeResolver = createRouteResolver(actions, view);
|
|
35945
|
+
m.route(container, settings.defaultRoute, routeResolver);
|
|
35740
35946
|
}
|
|
35741
35947
|
catch (e) {
|
|
35742
|
-
console.error("Exception during
|
|
35948
|
+
console.error("Exception during initialization: ", e);
|
|
35949
|
+
return "error";
|
|
35743
35950
|
}
|
|
35744
|
-
|
|
35745
|
-
return "error";
|
|
35951
|
+
return "success";
|
|
35746
35952
|
}
|
|
35747
35953
|
|
|
35748
|
-
const mountForUser$1 = async (state
|
|
35954
|
+
const mountForUser$1 = async (state) => {
|
|
35749
35955
|
// Return a DOM tree containing a mounted Netskrafl UI
|
|
35750
35956
|
// for the user specified in the state object
|
|
35751
35957
|
const { userEmail } = state;
|
|
@@ -35773,18 +35979,12 @@ const mountForUser$1 = async (state, tokenExpired) => {
|
|
|
35773
35979
|
root.className = "netskrafl-user";
|
|
35774
35980
|
return root;
|
|
35775
35981
|
}
|
|
35776
|
-
else if (loginResult === "expired") {
|
|
35777
|
-
// We need a new token from the Málstaður backend
|
|
35778
|
-
root.className = "netskrafl-expired";
|
|
35779
|
-
tokenExpired && tokenExpired(); // This causes a reload of the component
|
|
35780
|
-
return root;
|
|
35781
|
-
}
|
|
35782
35982
|
// console.error("Failed to mount Netskrafl UI for user", userEmail);
|
|
35783
35983
|
throw new Error("Failed to mount Netskrafl UI");
|
|
35784
35984
|
};
|
|
35785
35985
|
const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
35786
35986
|
const ref = React.createRef();
|
|
35787
|
-
const completeState = { ...
|
|
35987
|
+
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
35788
35988
|
const { userEmail } = completeState;
|
|
35789
35989
|
/*
|
|
35790
35990
|
useEffect(() => {
|
|
@@ -35817,7 +36017,7 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
|
35817
36017
|
return;
|
|
35818
36018
|
}
|
|
35819
36019
|
try {
|
|
35820
|
-
mountForUser$1(completeState
|
|
36020
|
+
mountForUser$1(completeState).then((div) => {
|
|
35821
36021
|
// Attach the div as a child of the container
|
|
35822
36022
|
// instead of any previous children
|
|
35823
36023
|
const container = ref.current;
|
|
@@ -35899,6 +36099,10 @@ const RiddleScore = {
|
|
|
35899
36099
|
else {
|
|
35900
36100
|
classes.push(".hot");
|
|
35901
36101
|
}
|
|
36102
|
+
// Add celebration class if the player achieved the best possible score
|
|
36103
|
+
if (score >= riddle.bestPossibleScore) {
|
|
36104
|
+
classes.push(".celebrate");
|
|
36105
|
+
}
|
|
35902
36106
|
}
|
|
35903
36107
|
return m("div" + classes.join(""), m("span.gata-dagsins-legend", displayText));
|
|
35904
36108
|
}
|
|
@@ -36240,6 +36444,84 @@ const GataDagsinsRightSide = {
|
|
|
36240
36444
|
}
|
|
36241
36445
|
};
|
|
36242
36446
|
|
|
36447
|
+
/*
|
|
36448
|
+
|
|
36449
|
+
gatadagsins-help.ts
|
|
36450
|
+
|
|
36451
|
+
Help dialog for Gáta Dagsins
|
|
36452
|
+
|
|
36453
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
36454
|
+
Author: Vilhjálmur Þorsteinsson
|
|
36455
|
+
|
|
36456
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
36457
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
36458
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
36459
|
+
|
|
36460
|
+
*/
|
|
36461
|
+
const GataDagsinsHelp = {
|
|
36462
|
+
view: (vnode) => {
|
|
36463
|
+
const closeHelp = vnode.attrs.onClose;
|
|
36464
|
+
return m(".modal-dialog.gatadagsins-help", m(".modal-content", [
|
|
36465
|
+
// Header with close button
|
|
36466
|
+
m(".modal-header", [
|
|
36467
|
+
m("h2", "Um Gátu dagsins"),
|
|
36468
|
+
m("button.close", {
|
|
36469
|
+
onclick: closeHelp,
|
|
36470
|
+
"aria-label": "Loka"
|
|
36471
|
+
}, m("span", { "aria-hidden": "true" }, "×"))
|
|
36472
|
+
]),
|
|
36473
|
+
// Body with help content
|
|
36474
|
+
m(".modal-body", [
|
|
36475
|
+
m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir að finna " +
|
|
36476
|
+
"stigahæsta orðið sem hægt er að mynda með gefnum stöfum."),
|
|
36477
|
+
m("h3", "Hvernig á að spila"),
|
|
36478
|
+
m("ul", [
|
|
36479
|
+
m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
|
|
36480
|
+
m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
|
|
36481
|
+
m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
|
|
36482
|
+
m("li", "Orðin verða að tengjast við stafi sem fyrir eru á borðinu."),
|
|
36483
|
+
m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
|
|
36484
|
+
m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
|
|
36485
|
+
]),
|
|
36486
|
+
m("h3", "Stigagjöf"),
|
|
36487
|
+
m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
|
|
36488
|
+
m("ul", [
|
|
36489
|
+
m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
|
|
36490
|
+
m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
|
|
36491
|
+
m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
|
|
36492
|
+
m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
|
|
36493
|
+
]),
|
|
36494
|
+
m("h3", "Hitamælir"),
|
|
36495
|
+
m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
|
|
36496
|
+
m("ul", [
|
|
36497
|
+
m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
|
|
36498
|
+
m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
|
|
36499
|
+
m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
|
|
36500
|
+
m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
|
|
36501
|
+
]),
|
|
36502
|
+
m("h3", "Ábendingar"),
|
|
36503
|
+
m("ul", [
|
|
36504
|
+
m("li", "Reyndu að nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
|
|
36505
|
+
m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
|
|
36506
|
+
m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
|
|
36507
|
+
m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
|
|
36508
|
+
]),
|
|
36509
|
+
m("h3", "Um leikinn"),
|
|
36510
|
+
m("p", [
|
|
36511
|
+
"Gáta dagsins er systkini ",
|
|
36512
|
+
m("a", { href: "https://netskrafl.is", target: "_blank" }, "Netskrafls"),
|
|
36513
|
+
", hins sívinsæla íslenska krossgátuleiks á netinu. ",
|
|
36514
|
+
"Leikurinn er þróaður af Miðeind ehf."
|
|
36515
|
+
]),
|
|
36516
|
+
]),
|
|
36517
|
+
// Footer with close button
|
|
36518
|
+
m(".modal-footer", m("button.btn.btn-primary", {
|
|
36519
|
+
onclick: closeHelp
|
|
36520
|
+
}, "Loka"))
|
|
36521
|
+
]));
|
|
36522
|
+
}
|
|
36523
|
+
};
|
|
36524
|
+
|
|
36243
36525
|
/*
|
|
36244
36526
|
|
|
36245
36527
|
GataDagsins.ts
|
|
@@ -36308,47 +36590,65 @@ const currentMoveState = (riddle) => {
|
|
|
36308
36590
|
}
|
|
36309
36591
|
return { selectedMoves, bestMove };
|
|
36310
36592
|
};
|
|
36311
|
-
const GataDagsins$1 = {
|
|
36593
|
+
const GataDagsins$1 = () => {
|
|
36312
36594
|
// A view of the Gáta Dagsins page
|
|
36313
|
-
|
|
36314
|
-
|
|
36315
|
-
|
|
36316
|
-
|
|
36317
|
-
const {
|
|
36318
|
-
|
|
36319
|
-
|
|
36595
|
+
let showHelp = false;
|
|
36596
|
+
return {
|
|
36597
|
+
oninit: (vnode) => {
|
|
36598
|
+
const { model, actions } = vnode.attrs.view;
|
|
36599
|
+
const { riddle } = model;
|
|
36600
|
+
if (!riddle) {
|
|
36601
|
+
const { date, locale } = vnode.attrs;
|
|
36602
|
+
// Initialize a fresh riddle object if it doesn't exist
|
|
36603
|
+
actions.fetchRiddle(date, locale);
|
|
36604
|
+
}
|
|
36605
|
+
// Initialize help dialog state
|
|
36606
|
+
showHelp = false;
|
|
36607
|
+
},
|
|
36608
|
+
view: (vnode) => {
|
|
36609
|
+
var _a;
|
|
36610
|
+
const { view } = vnode.attrs;
|
|
36611
|
+
const { model } = view;
|
|
36612
|
+
const { riddle } = model;
|
|
36613
|
+
const { selectedMoves, bestMove } = (riddle
|
|
36614
|
+
? currentMoveState(riddle)
|
|
36615
|
+
: { selectedMoves: [], bestMove: undefined });
|
|
36616
|
+
const toggleHelp = () => {
|
|
36617
|
+
showHelp = !showHelp;
|
|
36618
|
+
m.redraw();
|
|
36619
|
+
};
|
|
36620
|
+
return m("div.drop-target", {
|
|
36621
|
+
id: "gatadagsins-background",
|
|
36622
|
+
}, [
|
|
36623
|
+
// The main content area
|
|
36624
|
+
riddle ? m(".gatadagsins-container", [
|
|
36625
|
+
// Main display area with flex layout
|
|
36626
|
+
m(".gatadagsins-main", [
|
|
36627
|
+
// Board and rack component (left side)
|
|
36628
|
+
m(GataDagsinsBoardAndRack, { view }),
|
|
36629
|
+
// Right-side component with scores and comparisons
|
|
36630
|
+
m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
|
|
36631
|
+
// Blank dialog
|
|
36632
|
+
riddle.askingForBlank
|
|
36633
|
+
? m(BlankDialog, { game: riddle })
|
|
36634
|
+
: "",
|
|
36635
|
+
])
|
|
36636
|
+
]) : "",
|
|
36637
|
+
// The left margin elements: back button and info/help button
|
|
36638
|
+
// These elements appear after the main container for proper z-order
|
|
36639
|
+
// m(LeftLogo), // Currently no need for the logo for Gáta Dagsins
|
|
36640
|
+
// Show the Beginner component if the user is a beginner
|
|
36641
|
+
((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
|
|
36642
|
+
// Custom Info button for GataDagsins that shows help dialog
|
|
36643
|
+
m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
|
|
36644
|
+
// Help dialog and backdrop
|
|
36645
|
+
showHelp ? [
|
|
36646
|
+
m(".modal-backdrop", { onclick: (e) => { e.preventDefault(); } }),
|
|
36647
|
+
m(GataDagsinsHelp, { onClose: toggleHelp })
|
|
36648
|
+
] : "",
|
|
36649
|
+
]);
|
|
36320
36650
|
}
|
|
36321
|
-
}
|
|
36322
|
-
view: (vnode) => {
|
|
36323
|
-
const { view } = vnode.attrs;
|
|
36324
|
-
const { model } = view;
|
|
36325
|
-
const { riddle } = model;
|
|
36326
|
-
const { selectedMoves, bestMove } = (riddle
|
|
36327
|
-
? currentMoveState(riddle)
|
|
36328
|
-
: { selectedMoves: [], bestMove: undefined });
|
|
36329
|
-
return m("div.drop-target", {
|
|
36330
|
-
id: "gatadagsins-background",
|
|
36331
|
-
}, [
|
|
36332
|
-
// The main content area
|
|
36333
|
-
riddle ? m(".gatadagsins-container", [
|
|
36334
|
-
// Main display area with flex layout
|
|
36335
|
-
m(".gatadagsins-main", [
|
|
36336
|
-
// Board and rack component (left side)
|
|
36337
|
-
m(GataDagsinsBoardAndRack, { view }),
|
|
36338
|
-
// Right-side component with scores and comparisons
|
|
36339
|
-
m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
|
|
36340
|
-
// Blank dialog
|
|
36341
|
-
riddle.askingForBlank
|
|
36342
|
-
? m(BlankDialog, { game: riddle })
|
|
36343
|
-
: "",
|
|
36344
|
-
])
|
|
36345
|
-
]) : "",
|
|
36346
|
-
// The left margin elements: back button and info/help button
|
|
36347
|
-
// These elements appear after the main container for proper z-order
|
|
36348
|
-
m(LeftLogo),
|
|
36349
|
-
m(Info),
|
|
36350
|
-
]);
|
|
36351
|
-
}
|
|
36651
|
+
};
|
|
36352
36652
|
};
|
|
36353
36653
|
|
|
36354
36654
|
/*
|
|
@@ -36377,44 +36677,28 @@ async function main(state, container) {
|
|
|
36377
36677
|
console.error("No container element found");
|
|
36378
36678
|
return "error";
|
|
36379
36679
|
}
|
|
36380
|
-
// Set up Netskrafl backend server URLs
|
|
36381
|
-
setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
|
|
36382
36680
|
try {
|
|
36383
|
-
|
|
36384
|
-
|
|
36385
|
-
|
|
36386
|
-
|
|
36387
|
-
|
|
36388
|
-
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
36392
|
-
|
|
36393
|
-
|
|
36394
|
-
|
|
36395
|
-
// Everything looks OK:
|
|
36396
|
-
// Create the model, view and actions objects
|
|
36397
|
-
const settings = getSettings();
|
|
36398
|
-
const model = new Model(settings, state);
|
|
36399
|
-
const actions = new Actions(model);
|
|
36400
|
-
const view = new View(actions);
|
|
36401
|
-
const today = new Date().toISOString().split("T")[0];
|
|
36402
|
-
const locale = state.locale || "is_IS";
|
|
36403
|
-
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36404
|
-
m.mount(container, {
|
|
36405
|
-
view: () => m(GataDagsins$1, { view, date: today, locale }),
|
|
36406
|
-
});
|
|
36407
|
-
return "success";
|
|
36408
|
-
}
|
|
36681
|
+
// Skip initial login - authentication will happen lazily on first API call
|
|
36682
|
+
// Create the model, view and actions objects
|
|
36683
|
+
const settings = getSettings();
|
|
36684
|
+
const model = new Model(settings, state);
|
|
36685
|
+
const actions = new Actions(model);
|
|
36686
|
+
const view = new View(actions);
|
|
36687
|
+
const today = new Date().toISOString().split("T")[0];
|
|
36688
|
+
const locale = state.locale || "is_IS";
|
|
36689
|
+
// Mount the Gáta Dagsins UI using an anonymous closure component
|
|
36690
|
+
m.mount(container, {
|
|
36691
|
+
view: () => m(GataDagsins$1, { view, date: today, locale }),
|
|
36692
|
+
});
|
|
36409
36693
|
}
|
|
36410
36694
|
catch (e) {
|
|
36411
|
-
console.error("Exception during
|
|
36695
|
+
console.error("Exception during initialization: ", e);
|
|
36696
|
+
return "error";
|
|
36412
36697
|
}
|
|
36413
|
-
|
|
36414
|
-
return "error";
|
|
36698
|
+
return "success";
|
|
36415
36699
|
}
|
|
36416
36700
|
|
|
36417
|
-
const mountForUser = async (state
|
|
36701
|
+
const mountForUser = async (state) => {
|
|
36418
36702
|
// Return a DOM tree containing a mounted Gáta Dagsins UI
|
|
36419
36703
|
// for the user specified in the state object
|
|
36420
36704
|
const { userEmail } = state;
|
|
@@ -36442,18 +36726,12 @@ const mountForUser = async (state, tokenExpired) => {
|
|
|
36442
36726
|
root.className = "gatadagsins-user";
|
|
36443
36727
|
return root;
|
|
36444
36728
|
}
|
|
36445
|
-
else if (loginResult === "expired") {
|
|
36446
|
-
// We need a new token from the Málstaður backend
|
|
36447
|
-
root.className = "gatadagsins-expired";
|
|
36448
|
-
tokenExpired && tokenExpired(); // This causes a reload of the component
|
|
36449
|
-
return root;
|
|
36450
|
-
}
|
|
36451
36729
|
// console.error("Failed to mount Gáta Dagsins UI for user", userEmail);
|
|
36452
36730
|
throw new Error("Failed to mount Gáta Dagsins UI");
|
|
36453
36731
|
};
|
|
36454
36732
|
const GataDagsinsImpl = ({ state, tokenExpired }) => {
|
|
36455
36733
|
const ref = React.createRef();
|
|
36456
|
-
const completeState = { ...
|
|
36734
|
+
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
36457
36735
|
const { userEmail } = completeState;
|
|
36458
36736
|
React.useEffect(() => {
|
|
36459
36737
|
var _a;
|
|
@@ -36471,7 +36749,7 @@ const GataDagsinsImpl = ({ state, tokenExpired }) => {
|
|
|
36471
36749
|
return;
|
|
36472
36750
|
}
|
|
36473
36751
|
try {
|
|
36474
|
-
mountForUser(completeState
|
|
36752
|
+
mountForUser(completeState).then((div) => {
|
|
36475
36753
|
// Attach the div as a child of the container
|
|
36476
36754
|
// instead of any previous children
|
|
36477
36755
|
const container = ref.current;
|