1// assets/js/account/account.core.js 2document.addEventListener("DOMContentLoaded", function () { 3 window.VSH25Account = window.VSH25Account || { 4 dom: {}, 5 state: {}, 6 utils: {}, 7 api: {}, 8 ui: {}, 9 layout: {}, 10 calendar: {}, 11 wellbeing: {}, 12 video: {}, 13 flags: {}, 14 keys: {}, 15 selectors: {}, 16 }; 17 18 const A = window.VSH25Account; 19 if (A.__coreReady) return; 20 A.__coreReady = true; 21 22 const LS_KEY_HIDE_BIO_POPUP = "vsh25_hide_bio_popup"; 23 const LS_KEY_SIDEBAR_COLLAPSED = "vsh25_lk_sidebar_collapsed"; 24 const LS_KEY_SELECTED_DATE = "vsh25_selected_date"; 25 const LS_KEY_SELECTED_DAY = "vsh25_saved_day"; 26 27 const __VSH25_AJAX_ABORT = new Map(); 28 const __VSH25_OVERLAY_QS = ".preloader,#preloader,.loader,.page-loader,.overlay,.modal-backdrop"; 29 const __VSH25_OVERLAY_STALE_MS = 10000; 30 const __VSH25_OVERLAY_SWEEP_DELAY_MS = 180; 31 const __VSH25_OVERLAY_ERROR_FORCE_MIN_AGE_MS = 1200; 32 const __VSH25_OVERLAY_DEBUG = !!window.VSH25_DEBUG_OVERLAY; 33 34 const POPUPS_OPEN = "#ratingSelectionPopup.show, #wellbeingDiaryPopup.show"; 35 36 const currentMonthYear = document.getElementById("currentMonthYear"); 37 const wpTodayStr = currentMonthYear?.dataset.today; 38 39 let __VSH25_ACTIVE_API_REQUESTS = 0; 40 let __VSH25_OVERLAY_SWEEP_TIMER = null; 41 42 function localIsoDate(d = new Date()) { 43 return d.toLocaleDateString("sv-SE"); 44 } 45 46 let today = wpTodayStr ? new Date(wpTodayStr + "T00:00:00") : new Date(); 47 const TODAY_ISO = localIsoDate(today); 48 49 function isLocked(date) { 50 return (today - date) / 864e5 > 14; 51 } 52 53 // ÐÑÑÑавлÑем глобалÑнÑй Ñлаг Ð´Ð»Ñ Ñвободной пеÑемоÑки 54 window.vsh25AllowFreeSeek = Boolean(window.VSH25_RUNTIME && VSH25_RUNTIME.allowFreeSeek); 55 56 // Ð¡Ð¾Ñ ÑанÑем ÑÐ°Ð±Ð»Ð¾Ð½Ñ rating popup ÑÑазÑ, как в акÑÑалÑном монолиÑе 57 document.querySelectorAll(".rating-popup").forEach((p) => { 58 if (!p.dataset.tpl) p.dataset.tpl = p.innerHTML.trim(); 59 }); 60 61 function vsh25DebugOverlay(...args) { 62 if (!__VSH25_OVERLAY_DEBUG) return; 63 try { 64 // eslint-disable-next-line no-console 65 console.log("[vsh25][overlay]", ...args); 66 } catch {} 67 } 68 69 function vsh25IsLkContext() { 70 try { 71 return !!document.querySelector(".lk-calendar, .personal_wrapp, .lk-video, .course-video, .vshp"); 72 } catch { 73 return false; 74 } 75 } 76 77 function vsh25ClosestSafe(node, selector) { 78 try { 79 if (!node || !node.closest) return null; 80 return node.closest(selector); 81 } catch { 82 return null; 83 } 84 } 85 86 function vsh25HasOpenPopup() { 87 try { 88 return !!document.querySelector(POPUPS_OPEN); 89 } catch { 90 return false; 91 } 92 } 93 94 function vsh25OverlayLooksVisible(el) { 95 if (!el) return false; 96 97 try { 98 const style = window.getComputedStyle(el); 99 if (!style) return false; 100 if (style.display === "none") return false; 101 if (style.visibility === "hidden") return false; 102 if (Number(style.opacity || "1") === 0) return false; 103 104 const rect = typeof el.getBoundingClientRect === "function" ? el.getBoundingClientRect() : null; 105 if (rect && rect.width <= 0 && rect.height <= 0) return false; 106 107 return true; 108 } catch { 109 return false; 110 } 111 } 112 113 function vsh25ShouldSkipOverlayNode(el) { 114 if (!el) return true; 115 116 try { 117 if (vsh25ClosestSafe(el, ".vshp")) return true; 118 if (vsh25ClosestSafe(el, ".rating-popup.show")) return true; 119 if (vsh25ClosestSafe(el, "#wellbeingDiaryPopup.show")) return true; 120 if (vsh25ClosestSafe(el, ".modal.show")) return true; 121 if (el.matches && el.matches("[data-vsh25-overlay-ignore='1']")) return true; 122 } catch {} 123 124 return false; 125 } 126 127 function vsh25MarkOverlaySeen(el) { 128 if (!el || !el.dataset) return; 129 130 try { 131 if (!el.dataset.vsh25SeenAt) { 132 el.dataset.vsh25SeenAt = String(Date.now()); 133 } 134 } catch {} 135 } 136 137 function vsh25GetOverlayAgeMs(el) { 138 if (!el || !el.dataset) return 0; 139
140 try { 141 const seenAt = Number(el.dataset.vsh25SeenAt || 0) || 0; 142 if (!seenAt) return 0; 143 return Math.max(0, Date.now() - seenAt); 144 } catch { 145 return 0; 146 } 147 } 148 149 function vsh25HideOverlayNode(el) { 150 if (!el) return; 151 152 try { 153 el.style.display = "none"; 154 el.style.visibility = "hidden"; 155 el.style.opacity = "0"; 156 el.style.pointerEvents = "none"; 157 } catch {} 158 159 try { 160 if (el.dataset) { 161 el.dataset.vsh25HiddenAt = String(Date.now()); 162 } 163 } catch {} 164 } 165 166 function vsh25CollectOverlayNodes() { 167 try { 168 return Array.from(document.querySelectorAll(__VSH25_OVERLAY_QS)).filter((el) => { 169 if (!el) return false; 170 if (vsh25ShouldSkipOverlayNode(el)) return false; 171 if (!vsh25OverlayLooksVisible(el)) return false; 172 173 vsh25MarkOverlaySeen(el); 174 return true; 175 }); 176 } catch { 177 return []; 178 } 179 } 180 181 // UI fail-safe: иногда из-за оÑибок/Abort/ÑÑоÑÐ¾Ð½Ð½Ð¸Ñ ÑкÑипÑов "пÑелоадеÑ" оÑÑаÑÑÑÑ Ð¸ даÑÑ Ð±ÐµÐ»Ñй ÑкÑан. 182 // ÐÑ ÐРломаем UX: ÑкÑÑваем ÑолÑко ÑеалÑно завиÑÑий overlay, а не лÑбой overlay без ÑазбоÑа. 183 function vsh25ForceHideOverlays(reason, opts = {}) { 184 try { 185 if (!vsh25IsLkContext()) return 0; 186 if (vsh25HasOpenPopup()) return 0; 187 188 const force = !!opts.force; 189 const minAgeMs = Math.max(0, Number(Object.prototype.hasOwnProperty.call(opts, "minAgeMs") ? opts.minAgeMs : __VSH25_OVERLAY_STALE_MS) || 0); 190 191 const nodes = vsh25CollectOverlayNodes(); 192 if (!nodes.length) return 0; 193 194 if (!force && __VSH25_ACTIVE_API_REQUESTS > 0) { 195 vsh25DebugOverlay("skip hide: active requests", { 196 reason: reason || "", 197 activeRequests: __VSH25_ACTIVE_API_REQUESTS, 198 overlays: nodes.length, 199 }); 200 return 0; 201 } 202 203 let hiddenCount = 0; 204 205 nodes.forEach((el) => { 206 const ageMs = vsh25GetOverlayAgeMs(el); 207 208 if (!force && ageMs < minAgeMs) { 209 return; 210 } 211 212 if (force && minAgeMs > 0 && ageMs > 0 && ageMs < minAgeMs) { 213 return; 214 } 215 216 vsh25HideOverlayNode(el); 217 hiddenCount += 1; 218 }); 219 220 if (!hiddenCount) return 0; 221 222 // на вÑÑкий Ñнимаем lock, еÑли его деÑжал пÑÐµÐ»Ð¾Ð°Ð´ÐµÑ 223 try { 224 document.body && document.body.classList && document.body.classList.remove("lock"); 225 } catch {} 226 227 vsh25DebugOverlay("hidden stale overlays", { 228 reason: reason || "", 229 hiddenCount, 230 force, 231 minAgeMs, 232 }); 233 234 return hiddenCount; 235 } catch { 236 return 0; 237 } 238 } 239 240 function vsh25ScheduleOverlaySweep(reason, opts = {}) { 241 try { 242 if (__VSH25_OVERLAY_SWEEP_TIMER) { 243 clearTimeout(__VSH25_OVERLAY_SWEEP_TIMER); 244 __VSH25_OVERLAY_SWEEP_TIMER = null; 245 } 246 247 const delay = Math.max(0, Number(opts.delayMs || __VSH25_OVERLAY_SWEEP_DELAY_MS) || 0); 248 249 __VSH25_OVERLAY_SWEEP_TIMER = setTimeout(() => { 250 __VSH25_OVERLAY_SWEEP_TIMER = null; 251 vsh25ForceHideOverlays(reason, opts); 252 }, delay); 253 } catch {} 254 } 255 256 // еÑли пÑилеÑела глобалÑÐ½Ð°Ñ Ð¾Ñибка/необÑабоÑаннÑй пÑÐ¾Ð¼Ð¸Ñ â мÑгко пÑовеÑÑем stale overlay 257 window.addEventListener( 258 "error", 259 function () { 260 vsh25ScheduleOverlaySweep("window_error", { 261 force: true, 262 minAgeMs: __VSH25_OVERLAY_ERROR_FORCE_MIN_AGE_MS, 263 delayMs: 80, 264 }); 265 }, 266 { passive: true }, 267 ); 268 269 window.addEventListener( 270 "unhandledrejection", 271 function () { 272 vsh25ScheduleOverlaySweep("unhandledrejection", { 273 force: true, 274 minAgeMs: __VSH25_OVERLAY_ERROR_FORCE_MIN_AGE_MS, 275 delayMs: 80, 276 }); 277 }, 278 { passive: true }, 279 ); 280 281 // watchdog: еÑли overlay живÑÑ ÑлиÑком долго и запÑоÑов Ñже Ð½ÐµÑ â ÑбиÑаем 282 setTimeout(function () { 283 vsh25ForceHideOverlays("watchdog_10s", { 284 force: false, 285 minAgeMs: __VSH25_OVERLAY_STALE_MS, 286 }); 287 }, 10000); 288 289 setTimeout(function () { 290 vsh25ForceHideOverlays("watchdog_18s", { 291 force: true, 292 minAgeMs: 2000,
293 }); 294 }, 18000); 295 296 function toggleHidePopup(checked) { 297 const val = checked ? "false" : "true"; // Ñо, ÑÑо ÑеалÑно кладÑм в LS 298 localStorage.setItem(LS_KEY_HIDE_BIO_POPUP, val); 299 300 // ÑÐ¾Ñ Ð¶Ðµ ÑамÑй val оÑпÑавлÑем в newValue 301 window.dispatchEvent( 302 new StorageEvent("storage", { 303 key: LS_KEY_HIDE_BIO_POPUP, 304 newValue: val, 305 }), 306 ); 307 } 308 309 function setBodyLock() { 310 document.body.classList.toggle("lock", !!document.querySelector(POPUPS_OPEN)); 311 } 312 313 function debounce(func, wait) { 314 let timeout; 315 316 return function executedFunction(...args) { 317 const later = () => { 318 clearTimeout(timeout); 319 func(...args); 320 }; 321 322 clearTimeout(timeout); 323 timeout = setTimeout(later, wait); 324 }; 325 } 326 327 // ÐÑименÑем debounce к setBodyLock â ÑÑо еÑÑÑ Ð² акÑÑалÑном Ñайле 328 const debouncedSetBodyLock = debounce(setBodyLock, 100); 329 window.addEventListener("resize", debouncedSetBodyLock); 330 331 function monthFloor(d) { 332 return new Date(d.getFullYear(), d.getMonth(), 1); 333 } 334 335 function isSameDay(d1, d2) { 336 return d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear(); 337 } 338 339 function isValidDateFormat(dateStr) { 340 const regex = /^\d{4}-\d{2}-\d{2}$/; 341 if (!regex.test(dateStr)) return false; 342 const date = new Date(dateStr + "T00:00:00"); 343 return date instanceof Date && !isNaN(date); 344 } 345 346 function isDateInRange(dateStr) { 347 const date = new Date(dateStr + "T00:00:00"); 348 const diffDays = (today - date) / 864e5; 349 return diffDays <= 14 && date <= today; 350 } 351 352 function getTodayStorageKey() { 353 return wpTodayStr || TODAY_ISO || localIsoDate(today || new Date()); 354 } 355 356 function forgetStoredDate() { 357 try { 358 sessionStorage.removeItem(LS_KEY_SELECTED_DAY); 359 sessionStorage.removeItem(LS_KEY_SELECTED_DATE); 360 } catch {} 361 } 362 363 function getStoredDate() { 364 let savedDay = null; 365 let selectedDate = null; 366 367 try { 368 savedDay = sessionStorage.getItem(LS_KEY_SELECTED_DAY); 369 selectedDate = sessionStorage.getItem(LS_KEY_SELECTED_DATE); 370 } catch { 371 return null; 372 } 373 374 if (!savedDay || !selectedDate) return null; 375 376 if (savedDay !== getTodayStorageKey()) { 377 forgetStoredDate(); 378 return null; 379 } 380 381 if (!isValidDateFormat(selectedDate) || !isDateInRange(selectedDate)) { 382 forgetStoredDate(); 383 return null; 384 } 385 386 return selectedDate; 387 } 388 389 function saveSelectedDate(dateStr) { 390 if (!isValidDateFormat(dateStr) || !isDateInRange(dateStr)) return false; 391 392 try { 393 sessionStorage.setItem(LS_KEY_SELECTED_DATE, dateStr); 394 sessionStorage.setItem(LS_KEY_SELECTED_DAY, getTodayStorageKey()); 395 return true; 396 } catch { 397 return false; 398 } 399 } 400 401 function getUrlSelectedDate() { 402 try { 403 const params = new URLSearchParams(window.location.search || ""); 404 const selectedDate = String(params.get("date") || params.get("selected_date") || "").trim(); 405 406 if (isValidDateFormat(selectedDate) && isDateInRange(selectedDate)) { 407 return selectedDate; 408 } 409 } catch {} 410 411 return ""; 412 } 413 414 function getBootstrapSelectedDate(accountBootstrap = {}) { 415 const bootSelectedDate = String(accountBootstrap.selectedDate || "").trim(); 416 if (isValidDateFormat(bootSelectedDate) && isDateInRange(bootSelectedDate)) { 417 return bootSelectedDate; 418 } 419 420 const progress = accountBootstrap.progress && typeof accountBootstrap.progress === "object" ? accountBootstrap.progress : null; 421 if (progress) { 422 const progressDate = String(progress.selectedDate || progress.currentDate || "").trim(); 423 if (isValidDateFormat(progressDate) && isDateInRange(progressDate)) { 424 return progressDate; 425 } 426 } 427 428 return ""; 429 } 430 431 function getInitialSharedSelectedDate(accountBootstrap = {}) {
432 return getUrlSelectedDate() || getStoredDate() || getBootstrapSelectedDate(accountBootstrap) || TODAY_ISO; 433 } 434 435 function applyInitialSharedSelectedDate(accountBootstrap = {}) { 436 const selectedDate = getInitialSharedSelectedDate(accountBootstrap); 437 438 if (!isValidDateFormat(selectedDate) || !isDateInRange(selectedDate)) { 439 return ""; 440 } 441 442 accountBootstrap.selectedDate = selectedDate; 443 444 if (accountBootstrap.progress && typeof accountBootstrap.progress === "object") { 445 accountBootstrap.progress.selectedDate = selectedDate; 446 accountBootstrap.progress.currentDate = selectedDate; 447 } 448 449 saveSelectedDate(selectedDate); 450 451 return selectedDate; 452 } 453 454 function saveSelectedDate(dateStr) { 455 if (isValidDateFormat(dateStr) && isDateInRange(dateStr)) { 456 sessionStorage.setItem("vsh25_selected_date", dateStr); 457 sessionStorage.setItem("vsh25_saved_day", wpTodayStr); 458 } 459 } 460 461 function errorText(key) { 462 const messages = { 463 unknownError: ['Ðе ÑдалоÑÑ Ð²ÑполниÑÑ Ð´ÐµÐ¹ÑÑвие. ÐопÑобÑйÑе еÑÑ Ñаз.', 'Could not complete this action. Please try again.'], 464 networkError: ['Ðе ÑдалоÑÑ ÑвÑзаÑÑÑÑ Ñ ÑеÑвеÑом. ÐÑовеÑÑÑе Ñоединение и попÑобÑйÑе еÑÑ Ñаз.', 'Could not connect. Check your connection and try again.'], 465 progressBarMissing: ['Ðе ÑдалоÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°ÑÑ Ð¿ÑогÑеÑÑ. ÐбновиÑе ÑÑÑаниÑÑ.', 'Could not display your progress. Reload the page.'], 466 calendar_not_found: ['Ðе ÑдалоÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°ÑÑ ÐºÐ°Ð»ÐµÐ½Ð´Ð°ÑÑ. ÐбновиÑе ÑÑÑаниÑÑ.', 'Could not display the calendar. Reload the page.'], 467 dateElementNotFound: ['Ðе ÑдалоÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°ÑÑ Ð²ÑбÑаннÑй денÑ. ÐбновиÑе ÑÑÑаниÑÑ.', 'Could not display the selected day. Reload the page.'], 468 dateFormatError: ['Ðе ÑдалоÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°ÑÑ Ð²ÑбÑаннÑй денÑ. ÐопÑобÑйÑе вÑбÑаÑÑ ÐµÐ³Ð¾ еÑÑ Ñаз.', 'Could not display the selected day. Please select it again.'], 469 progressUpdateError: ['Ðе ÑдалоÑÑ ÑÐ¾Ñ ÑаниÑÑ Ð¿ÑогÑеÑÑ. ÐÑовеÑÑÑе Ñоединение и попÑобÑйÑе еÑÑ Ñаз.', 'Could not save your progress. Check your connection and try again.'], 470 progressLoadError: ['Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð¿ÑогÑеÑÑ. ÐопÑобÑйÑе еÑÑ Ñаз.', 'Could not load your progress. Please try again.'], 471 progressNetworkError: ['Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð¿ÑогÑеÑÑ. ÐÑовеÑÑÑе Ñоединение и попÑобÑйÑе еÑÑ Ñаз.', 'Could not load your progress. Check your connection and try again.'], 472 wellbeingHistoryError: ['Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð´Ð½ÐµÐ²Ð½Ð¸Ðº. ÐопÑобÑйÑе еÑÑ Ñаз.', 'Could not load your diary. Please try again.'], 473 ratingError: ['Ðе ÑдалоÑÑ ÑÐ¾Ñ ÑаниÑÑ Ð¾ÑенкÑ. ÐопÑобÑйÑе еÑÑ Ñаз.', 'Could not save your rating. Please try again.'], 474 ratingSaved: ['ÐÑенка ÑÐ¾Ñ Ñанена.', 'Rating saved.'], 475 bioprogramVideoNetworkError: ['Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð±Ð¸Ð¾Ð¿ÑогÑаммÑ. ÐÑовеÑÑÑе Ñоединение и попÑобÑйÑе еÑÑ Ñаз.', 'Could not load the biological program. Check your connection and try again.'], 476 tabletVideoNetworkError: ['Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ ÑиÑÑовÑÑ ÑаблеÑкÑ. ÐÑовеÑÑÑе Ñоединение и попÑобÑйÑе еÑÑ Ñаз.', 'Could not load the digital tablet. Check your connection and try again.'], 477 noVideoMsg: ['Ðидео Ð´Ð»Ñ Ð²ÑбÑанной даÑÑ Ð¿Ð¾ÐºÐ° недоÑÑÑпно. ÐÑбеÑиÑе дÑÑгой денÑ.', 'The video for this date is not available yet. Choose another day.'], 478 }; 479 const message = Object.prototype.hasOwnProperty.call(messages, key) ? messages[key] : messages.unknownError; 480 return message[/^en(?:[-_]|$)/i.test(document.documentElement?.lang || '') ? 1 : 0]; 481 } 482 483 function createNotificationsContainer() { 484 const container = document.createElement("div"); 485 container.id = "notifications-container"; 486 container.style.cssText = 487 "position: fixed; bottom: calc(20px + env(safe-area-inset-bottom)); right: 16px; max-width: calc(100vw - 32px); z-index: 9999; display: flex; flex-direction: column; align-items: flex-end; gap: 10px;"; 488 document.body.appendChild(container); 489 return container; 490 } 491 492 function showNotification(message, type = "info") { 493 const notificationsContainer = document.getElementById("notifications-container") || createNotificationsContainer(); 494 if (!notificationsContainer) return; 495 496 const notification = document.createElement("div"); 497 notification.className = `notification vsh25-customer-notice ${type === "error" ? "error" : ""}`;
498 notification.setAttribute("role", type === "error" ? "alert" : "status"); 499 const messageText = document.createElement("span"); 500 messageText.textContent = typeof message === "string" ? message : errorText("unknownError"); 501 const closeButton = document.createElement("button"); 502 closeButton.className = "close-btn"; 503 closeButton.type = "button"; 504 closeButton.setAttribute("aria-label", /^en(?:[-_]|$)/i.test(document.documentElement?.lang || '') ? "Close notification" : "ÐакÑÑÑÑ Ñведомление"); 505 notification.appendChild(messageText); 506 notification.appendChild(closeButton); 507 508 if (notificationsContainer.firstChild) { 509 notificationsContainer.insertBefore(notification, notificationsContainer.firstChild); 510 } else { 511 notificationsContainer.appendChild(notification); 512 } 513 514 closeButton.addEventListener("click", () => notification.remove()); 515 516 setTimeout(() => notification.remove(), 10000); 517 } 518 519 function apiPost(action, params = {}, opts = {}) { 520 const { showLoader = true, timeoutMs = 12000, retries = 3, retryBaseDelayMs = 450, retryStatuses = [429, 500, 502, 503, 504], abortKey = null } = opts || {}; 521 522 const ajaxUrl = ajax_object && ajax_object.ajaxurl ? ajax_object.ajaxurl : ""; 523 if (!ajaxUrl) return Promise.reject(new Error("ajaxurl_missing")); 524 525 const body = new URLSearchParams({ action, ...params }); 526 const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 527 const ms = Math.max(1000, Number(timeoutMs) || 0); 528 529 // One controller per request (keeps retries tied to a single cancellation/timeout). 530 const ctrl = typeof AbortController === "function" ? new AbortController() : null; 531 532 // Abort previous request with same key (latest-wins). 533 if (abortKey && ctrl) { 534 const prev = __VSH25_AJAX_ABORT.get(abortKey); 535 if (prev && prev.ctrl && !prev.ctrl.signal.aborted) { 536 try { 537 prev.ctrl.abort(); 538 } catch {} 539 } 540 __VSH25_AJAX_ABORT.set(abortKey, { ctrl, at: Date.now() }); 541 } 542 543 const t = ctrl 544 ? setTimeout(() => { 545 try { 546 ctrl.abort(); 547 } catch {} 548 }, ms) 549 : null; 550 551 const fetchAttempt = async (attempt) => { 552 try { 553 const res = await fetch(ajaxUrl, { 554 method: "POST", 555 credentials: "same-origin", 556 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 557 body, 558 signal: ctrl ? ctrl.signal : undefined, 559 }); 560 561 if (!res.ok && retryStatuses.includes(res.status) && attempt < retries) { 562 const backoff = retryBaseDelayMs * Math.pow(2, attempt); 563 const jitter = Math.floor(Math.random() * 180); 564 await sleep(backoff + jitter); 565 return fetchAttempt(attempt + 1); 566 } 567 568 return res; 569 } catch (e) { 570 // If aborted (timeout or superseded), surface a stable error code for callers to ignore. 571 if (ctrl && ctrl.signal && ctrl.signal.aborted) { 572 const err = new Error("aborted"); 573 err.code = "aborted"; 574 throw err; 575 } 576 577 if (attempt < retries) { 578 const backoff = retryBaseDelayMs * Math.pow(2, attempt); 579 const jitter = Math.floor(Math.random() * 180); 580 await sleep(backoff + jitter); 581 return fetchAttempt(attempt + 1); 582 } 583 584 throw e; 585 } 586 }; 587 588 if (showLoader) { 589 __VSH25_ACTIVE_API_REQUESTS += 1; 590 591 if (typeof showPreloader === "function") { 592 try { 593 showPreloader(true); 594 } catch (e) { 595 vsh25ScheduleOverlaySweep("showPreloader_true_throw", { 596 force: true, 597 minAgeMs: __VSH25_OVERLAY_ERROR_FORCE_MIN_AGE_MS, 598 delayMs: 80, 599 }); 600 } 601 } 602 } 603 604 return fetchAttempt(0).finally(() => { 605 if (t) clearTimeout(t); 606 607 if (abortKey && ctrl) { 608 const cur = __VSH25_AJAX_ABORT.get(abortKey); 609 if (cur && cur.ctrl === ctrl) __VSH25_AJAX_ABORT.delete(abortKey); 610 } 611 612 if (showLoader) { 613 __VSH25_ACTIVE_API_REQUESTS = Math.max(0, __VSH25_ACTIVE_API_REQUESTS - 1); 614 615 if (typeof showPreloader === "function") { 616 try { 617 showPreloader(false); 618 } catch (e) { 619 vsh25ScheduleOverlaySweep("showPreloader_false_throw", { 620 force: true, 621 minAgeMs: __VSH25_OVERLAY_ERROR_FORCE_MIN_AGE_MS, 622 delayMs: 80, 623 }); 624 } 625 } 626 627 vsh25ScheduleOverlaySweep("apiPost_finally", { 628 force: false, 629 minAgeMs: 1200, 630 delayMs: __VSH25_OVERLAY_SWEEP_DELAY_MS, 631 }); 632 } 633 }); 634 } 635 636 function abortApiRequest(abortKey) {
637 const key = String(abortKey || ""); 638 if (!key || !__VSH25_AJAX_ABORT.has(key)) return false; 639 640 const entry = __VSH25_AJAX_ABORT.get(key); 641 let aborted = false; 642 643 if (entry && entry.ctrl && entry.ctrl.signal && !entry.ctrl.signal.aborted) { 644 try { 645 entry.ctrl.abort(); 646 aborted = true; 647 } catch {} 648 } 649 650 __VSH25_AJAX_ABORT.delete(key); 651 return aborted; 652 } 653 654 function abortApiRequestsByPrefix(prefix) { 655 const normalizedPrefix = String(prefix || ""); 656 if (!normalizedPrefix) return 0; 657 658 let abortedCount = 0; 659 Array.from(__VSH25_AJAX_ABORT.keys()).forEach((key) => { 660 if (!String(key).startsWith(normalizedPrefix)) return; 661 if (abortApiRequest(key)) abortedCount += 1; 662 }); 663 664 return abortedCount; 665 } 666 667 A.keys.LS_KEY_HIDE_BIO_POPUP = LS_KEY_HIDE_BIO_POPUP; 668 A.keys.LS_KEY_SIDEBAR_COLLAPSED = LS_KEY_SIDEBAR_COLLAPSED; 669 A.keys.LS_KEY_SELECTED_DATE = LS_KEY_SELECTED_DATE; 670 A.keys.LS_KEY_SELECTED_DAY = LS_KEY_SELECTED_DAY; 671 672 A.selectors.POPUPS_OPEN = POPUPS_OPEN; 673 A.selectors.OVERLAY_QS = __VSH25_OVERLAY_QS; 674 675 A.flags.allowFreeSeek = window.vsh25AllowFreeSeek; 676 A.flags.hasCalendar = !!document.querySelector(".lk-calendar"); 677 678 A.dom.currentMonthYear = currentMonthYear; 679 680 A.state.wpTodayStr = wpTodayStr; 681 A.state.today = today; 682 A.state.TODAY_ISO = TODAY_ISO; 683 684 const accountBootstrap = window.VSH25_ACCOUNT_BOOTSTRAP && typeof window.VSH25_ACCOUNT_BOOTSTRAP === "object" ? window.VSH25_ACCOUNT_BOOTSTRAP : {}; 685 686 A.state.accountBootstrap = accountBootstrap; 687 const initialSharedSelectedDate = applyInitialSharedSelectedDate(accountBootstrap); 688 689 if (!A.state.watchHistory || typeof A.state.watchHistory !== "object") { 690 A.state.watchHistory = {}; 691 } 692 693 if (!A.state.wellbeingHistory || typeof A.state.wellbeingHistory !== "object") { 694 A.state.wellbeingHistory = {}; 695 } 696 697 if (!A.state.newVideoMap || typeof A.state.newVideoMap !== "object") { 698 A.state.newVideoMap = {}; 699 } 700 701 if (accountBootstrap.progress && typeof accountBootstrap.progress === "object") { 702 if (accountBootstrap.progress.watchHistory && typeof accountBootstrap.progress.watchHistory === "object") { 703 A.state.watchHistory = accountBootstrap.progress.watchHistory; 704 } 705 706 if (accountBootstrap.progress.newVideoMap && typeof accountBootstrap.progress.newVideoMap === "object") { 707 A.state.newVideoMap = accountBootstrap.progress.newVideoMap; 708 } 709 710 if (accountBootstrap.progress.currentDate && typeof accountBootstrap.progress.currentDate === "string") { 711 A.state.currentDate = new Date(accountBootstrap.progress.currentDate + "T00:00:00"); 712 } 713 } 714 715 if (accountBootstrap.wellbeingHistory && typeof accountBootstrap.wellbeingHistory === "object") { 716 A.state.wellbeingHistory = accountBootstrap.wellbeingHistory; 717 A.state.wellbeingHistoryBootstrapped = true; 718 } 719 720 if (initialSharedSelectedDate) { 721 const sharedDate = new Date(initialSharedSelectedDate + "T00:00:00"); 722 723 if (!Number.isNaN(sharedDate.getTime())) { 724 A.state.currentDate = new Date(sharedDate); 725 A.state.wellbeingCurrentDate = new Date(sharedDate); 726 } 727 } else if (accountBootstrap.selectedDate && typeof accountBootstrap.selectedDate === "string") { 728 const bootstrappedDate = new Date(accountBootstrap.selectedDate + "T00:00:00"); 729 730 if (!Number.isNaN(bootstrappedDate.getTime())) { 731 A.state.currentDate = new Date(bootstrappedDate); 732 A.state.wellbeingCurrentDate = new Date(bootstrappedDate); 733 } 734 } 735 736 if (!(A.state.shownNewVideoNotices instanceof Set)) { 737 A.state.shownNewVideoNotices = new Set(); 738 } 739 740 if (typeof A.state.bioprogramNotified !== "boolean") { 741 A.state.bioprogramNotified = false; 742 } 743 744 if (typeof A.state.bioprogramWatched !== "boolean") { 745 A.state.bioprogramWatched = false; 746 } 747 748 if (typeof A.state.tabletWatched !== "boolean") { 749 A.state.tabletWatched = false; 750 } 751 752 if (typeof A.state.initialVideosLoaded !== "boolean") { 753 A.state.initialVideosLoaded = false;
754 } 755 756 if (!("currentDate" in A.state)) { 757 A.state.currentDate = null; 758 } 759 760 if (!("wellbeingCurrentDate" in A.state)) { 761 A.state.wellbeingCurrentDate = null; 762 } 763 764 if (!("wellbeingInFlight" in A.state)) { 765 A.state.wellbeingInFlight = null; 766 } 767 768 A.utils.localIsoDate = localIsoDate; 769 A.utils.monthFloor = monthFloor; 770 A.utils.isSameDay = isSameDay; 771 A.utils.isLocked = isLocked; 772 A.utils.isValidDateFormat = isValidDateFormat; 773 A.utils.isDateInRange = isDateInRange; 774 A.utils.getStoredDate = getStoredDate; 775 A.utils.saveSelectedDate = saveSelectedDate; 776 A.utils.getUrlSelectedDate = getUrlSelectedDate; 777 A.utils.debounce = debounce; 778 779 A.ui.forceHideOverlays = vsh25ForceHideOverlays; 780 A.ui.scheduleOverlaySweep = vsh25ScheduleOverlaySweep; 781 A.ui.setBodyLock = setBodyLock; 782 A.ui.debouncedSetBodyLock = debouncedSetBodyLock; 783 A.ui.toggleHidePopup = toggleHidePopup; 784 A.ui.showNotification = showNotification; 785 A.ui.errorText = errorText; 786 A.ui.createNotificationsContainer = createNotificationsContainer; 787 788 A.api.post = apiPost; 789 A.api.apiPost = apiPost; 790 A.api.abortRequest = abortApiRequest; 791 A.api.abortRequestsByPrefix = abortApiRequestsByPrefix; 792 793 window.toggleHidePopup = toggleHidePopup; 794 window.showNotification = showNotification; 795});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.