1window.YTT = window.YTT || {}; 2 3const MILESTONE_CONFIG = [ 4 { days: 7, itemId: 'phaptuong_cal7', src: 'images/gift_dang_nhap/gift1.webm', name: 'Hoà ng Cá»±c Kim Thân', desc: 'Pháp tưá»ng ngưng tụ từ vạn nÄm tu vi Äạo gia. Toà n thân tá»a ra vạn trượng kim quang chói lá»i, vạn tà bất xâm, thá» hiá»n phong thái chà tôn cá»§a báºc Äại nÄng ngá» Äạo.' }, 5 { days: 14, itemId: 'phaptuong_cal14', src: 'images/gift_dang_nhap/gift2.webm', name: 'Thần Ma Nhất Niá»m', desc: 'Dá» tượng sinh ra khi tâm trà giằng xé giữa Pháºt môn và Ma Äạo. Má»t ná»a xót thương Äá» thế, má»t ná»a sát phạt quyết Äoán. Uy lá»±c nghá»ch thiên, phá vỡ má»i quy tắc luân há»i.' }, 6 { days: 21, itemId: 'phaptuong_cal21', src: 'images/gift_dang_nhap/gift3.webm', name: 'Phần Thiên Chân Há»a', desc: 'Há»a diá» m bùng phát từ Äan Äiá»n, ngưng tụ há»a linh khà tinh thuần nhất. ThÃch hợp cho ngưá»i tu luyá»n há»a há» công pháp váºn khà Äá»t phá, mượn lá»a thiêu rụi má»i tâm ma.' }, 7 { days: 28, itemId: 'phaptuong_cal28', src: 'images/gift_dang_nhap/gift5.webm', name: 'BÃch Thá»§y Uẩn Thần', desc: 'Hấp thụ linh thá»§y từ cá»u u hà n tuyá»n, giữ tâm trà luôn tÄ©nh lặng như mặt há» không gợn sóng. Pháp môn dưỡng thần báºc nhất, giúp xoa dá»u kinh mạch và cưá»ng hóa thần thức.' }, 8 { days: 50, itemId: 'phaptuong_cal50', src: 'images/gift_dang_nhap/gift6.webm', name: 'Vạn Tá»± Kim Tráºn', desc: 'Hình ảnh tu chân giả chìm và o cõi má»ng ngá» Äạo, xung quanh là tráºn pháp chân ngôn xoay chuyá»n liên tục. Giúp khai má» linh trÃ, gia tÄng tá»c Äá» lÄ©nh ngá» thiên cÆ¡.' }, 9 { days: 80, itemId: 'phaptuong_cal80', src: 'images/gift_dang_nhap/gift8.webm', name: 'XÃch Viêm Há» Thá»', desc: 'Váºn chuyá»n bà pháp tạo ra má»t lá»p huyết diá» m hình cầu bao bá»c bảo há» toà n thân. Vừa là bức tưá»ng phòng ngá»± kiên cá», vừa có khả nÄng phản chấn má»i tà khà xâm nháºp.' }, 10 { days: 100, itemId: 'phaptuong_cal100', src: 'images/gift_dang_nhap/gift9.webm', name: 'U Minh Huyết Sát', desc: 'Pháp tưá»ng hung ác...', highlight: true }, 11 { days: 150, itemId: 'phaptuong_cal150', src: 'images/gift_dang_nhap/gift4.webm', name: 'Má»c 150 Ngà y', desc: 'ChÄm chá» tu luyá»n 150 ngà y', highlight: true }, 12 { days: 200, itemId: 'phaptuong_cal200', src: 'images/gift_dang_nhap/gift7.webm', name: 'Má»c 200 Ngà y', desc: 'ChÄm chá» tu luyá»n 200 ngà y', highlight: true }, 13 { days: 250, itemId: 'phaptuong_cal250', src: 'images/gift_dang_nhap/gift10.webm', name: 'Má»c 250 Ngà y', desc: 'ChÄm chá» tu luyá»n 250 ngà y', highlight: true }, 14 { days: 300, itemId: 'phaptuong_cal300', src: 'images/gift_dang_nhap/gift11.webm', name: 'Má»c 300 Ngà y', desc: 'ChÄm chá» tu luyá»n 300 ngà y', highlight: true }, 15 { days: 365, itemId: 'phaptuong_cal365', src: 'images/gift_dang_nhap/gift12.webm', name: 'Má»c 1 NÄm', desc: 'Tu luyá»n tròn 1 nÄm kiên trì', highlight: true }, 16]; 17 18const renderMilestones = (totalCheckedIn, claimedMilestones = []) => { 19 const inner = document.getElementById('milestone-scroll-inner'); 20 if (!inner) return; 21 22 // Xóa gift nodes cÅ©, giữ lại 2 progress line 23 inner.querySelectorAll('.gift-node').forEach(n => n.remove()); 24 25 const maxDays = MILESTONE_CONFIG[MILESTONE_CONFIG.length - 1].days; 26 const percent = Math.min((totalCheckedIn / maxDays) * 100, 100); 27 const fill = document.getElementById('cal-progress-fill'); 28 if (fill) fill.style.width = percent + '%'; 29 30 MILESTONE_CONFIG.forEach(m => { 31 const reached = totalCheckedIn >= m.days; 32 const claimed = claimedMilestones.includes(m.itemId); 33 const showBtn = reached && !claimed; 34 35 const node = document.createElement('div'); 36 node.className = 'gift-node' + (reached ? ' reached' : ''); 37 node.id = `milestone-node-${m.days}`; 38 node.innerHTML = ` 39 <div class="gift-video-wrapper" title="${m.name}: ${m.desc}"> 40 ${m.src.endsWith('.webm') || m.src.endsWith('.mp4') 41 ? `<video class="milestone-vid" autoplay loop muted playsinline data-src="${m.src}"></video>` 42 : `<img class="milestone-vid" src="${m.src}" alt="Gift">`} 43 </div>
44 <span class="gift-label" ${m.highlight ? 'style="color: red; font-weight: bold;"' : ''}>${m.days} Ngà y</span> 45 ${showBtn ? `<button class="btn-claim-milestone" onclick="window.YTT.Calendar.doClaimMilestone(${m.days})">NHẬN</button>` : ''} 46 ${claimed ? `<span class="milestone-claimed-badge"><i class="fas fa-check"></i></span>` : ''} 47 `; 48 inner.appendChild(node); 49 }); 50 napVideoQua(inner); 51 52 // Auto scroll Äến má»c hiá»n tại 53 const allReached = inner.querySelectorAll('.gift-node.reached'); 54 const lastReached = allReached.length ? allReached[allReached.length - 1] : null; 55 if (lastReached) lastReached.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); 56}; 57 58// [22/09/2026] Video quà Äiá»m danh phát từ BLOB, không gán thẳng URL: Firebase Hosting nén gzip cả 59// video/webm và tÃnh Range trên luá»ng Äã nén â Chrome nháºn Content-Range sai cỡ â Äá» console 60// "416 Range Not Satisfiable" má»i lần má» lá»ch và vòng lặp có thá» chết sau lượt Äầu (cùng gá»c vá»i 61// ná»n phòng chá» trò chÆ¡i â xem napNenPhong trong js/trochoi-ui.js). fetch() Äá» trình duyá»t tá»± giải 62// nén Äúng, phát từ bá» nhá»; blob giữ theo src Äá» má» lá»ch lần sau không tải lại. 63const _blobVideoQua = {}; 64function napVideoQua(goc) { 65 if (!goc || !window.fetch || !window.URL || !URL.createObjectURL) { 66 (goc ? goc.querySelectorAll('video[data-src]') : []).forEach((v) => { v.src = v.dataset.src; }); 67 return; 68 } 69 goc.querySelectorAll('video[data-src]').forEach((v) => { 70 const src = v.dataset.src; 71 const dat = (url) => { v.src = url; const p = v.play && v.play(); if (p && p.catch) p.catch(() => {}); }; 72 if (_blobVideoQua[src]) { dat(_blobVideoQua[src]); return; } 73 fetch(src).then((r) => (r.ok ? r.blob() : null)) 74 .then((b) => { if (!b) return; _blobVideoQua[src] = URL.createObjectURL(b); dat(_blobVideoQua[src]); }) 75 .catch(() => {}); 76 }); 77} 78 79window.YTT.Calendar = (function() { 80 let isLoading = false; 81 let currentUserDoc = null; 82 83 // Cache Firebase Functions module â import 1 lần, dùng mãi 84 let _fnModulePromise = null; 85 const _getFnModule = () => { 86 if (!_fnModulePromise) { 87 _fnModulePromise = Promise.all([ 88 import("https://www.gstatic.com/firebasejs/10.7.1/firebase-functions.js"), 89 import('./firebase-config.js'), 90 ]).then(([fnMod, cfgMod]) => ({ 91 getFunctions: fnMod.getFunctions, 92 httpsCallable: fnMod.httpsCallable, 93 app: cfgMod.app, 94 })); 95 } 96 return _fnModulePromise; 97 }; 98 99 const getVNTimeVars = () => { 100 const now = new Date(); 101 const vnTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); 102 const vnToday = `${vnTime.getUTCFullYear()}-${String(vnTime.getUTCMonth()+1).padStart(2, '0')}-${String(vnTime.getUTCDate()).padStart(2, '0')}`; 103 const vnMonth = vnToday.substring(0, 7); // "YYYY-MM" 104 const vnDay = vnTime.getUTCDate(); 105 return { vnTime, vnToday, vnMonth, vnDay }; 106 }; 107 108 const loadCSS = () => { 109 if (document.getElementById("calendar-css")) return; 110 const link = document.createElement("link"); 111 link.id = "calendar-css"; 112 link.rel = "stylesheet"; 113 link.href = "css/calendar.css?v=23.59"; 114 document.head.appendChild(link); 115 }; 116 117 // [FEATURE FLAG GUARD] â Chặn Lá»ch Äiá»m Danh khi flag bá» tắt 118 const triggerOpenModal = () => { 119 window.guardFeature('calendar', () => { 120 const user = window.YTT_currentUser || null; 121 if (!user) { 122 window.showHippoLoginModal ? window.showHippoLoginModal("Lá»ch Äiá»m Danh") : alert("Vui lòng ÄÄng nháºp!"); 123 return; 124 } 125 // Gá»i Lazy Load qua Main.js 126 window.YTT.Main.loadModalOnDemand("calendarModal", openModal); 127 }); 128 }; 129 130 const openModal = async () => { 131 loadCSS(); 132 const calModal = document.getElementById("calendarModal"); 133 const calBackdrop = document.getElementById("calendar-backdrop"); 134 if (!calModal) { 135 window.xianxiaAlert ? window.xianxiaAlert("Tráºn pháp Äiá»m Danh Äang khá»i Äá»ng, xin Äợi 1 giây!") : alert("Äang tải..."); 136 return; 137 } 138 calBackdrop.classList.add("show"); 139 setTimeout(() => calModal.classList.add("open"), 10); 140 141 // Render ngay láºp tức từ dữ liá»u Äã có trong memory â không chá» Firestore 142 if (window.CURRENT_USER_DATA) { 143 currentUserDoc = window.CURRENT_USER_DATA;
144 renderCalendar(); 145 } 146 147 // Background refresh Äá» Äá»ng bá», không block UI 148 fetchUserCheckIns(); 149 // Pre-warm Firebase Functions module 150 _getFnModule(); 151 }; 152 153 const closeModal = () => { 154 const calModal = document.getElementById("calendarModal"); 155 const calBackdrop = document.getElementById("calendar-backdrop"); 156 if(calModal) calModal.classList.remove("open"); 157 if(calBackdrop) calBackdrop.classList.remove("show"); 158 }; 159 160 const fetchUserCheckIns = async () => { 161 const user = window.YTT_currentUser; 162 if (!user) return; 163 try { 164 const { db, doc, getDoc } = await import('./firebase-config.js'); 165 const docRef = doc(db, "users", user.uid); 166 const docSnap = await getDoc(docRef); 167 if (docSnap.exists()) { 168 currentUserDoc = docSnap.data(); 169 renderCalendar(); 170 } 171 } catch (e) { 172 console.error("Lá»i lấy dữ liá»u Äiá»m danh:", e); 173 } 174 }; 175 176 const renderCalendar = () => { 177 const container = document.getElementById("cal-days-container"); 178 if (!container) return; 179 180 const monthYearEl = document.getElementById("calendarMonthTitle"); 181 const btnDaily = document.getElementById("btn-daily-checkin"); 182 const streakEl = document.getElementById("cal-streak"); 183 184 const { vnTime, vnToday, vnMonth, vnDay } = getVNTimeVars(); 185 186 const year = vnTime.getUTCFullYear();
187 const month = vnTime.getUTCMonth(); // 0-based 188 if(monthYearEl) { 189 document.getElementById('cal-month-skeleton')?.style.setProperty('display','none'); 190 monthYearEl.textContent = `Tháng ${String(month + 1).padStart(2, '0')}/${year} - Hôm nay: ${String(vnDay).padStart(2, '0')}/${String(month + 1).padStart(2, '0')}`; 191 } 192 193 const checkInMap = currentUserDoc?.checkIns || {}; 194 const monthKey = `${year}-${String(month+1).padStart(2, '0')}`; 195 const checkInsThisMonth = checkInMap[monthKey] || []; 196 const lastCheckIn = currentUserDoc?.lastCheckInDate; 197 198 // [NÃNG CẤP THUẬT TOÃN STREAK V3]: Äếm ngược "Vượt Thá»i Gian" qua các tháng (Không bá» Äứt chuá»i ngá»n lá»a má»ng 1) 199 let streak = 0; 200 let scanDate = new Date(Date.UTC(year, month, vnDay)); 201 let checkingToday = true; 202 203 // Vòng lặp Äếm lùi an toà n, tá»± Äá»ng break khi thấy Äứt chuá»i 204 while (streak < 3650) { // Giá»i hạn max 10 nÄm Äá» phòng há» lặp vô hạn 205 const sYear = scanDate.getUTCFullYear(); 206 const sMonth = scanDate.getUTCMonth(); 207 const sDay = scanDate.getUTCDate(); 208 const sMonthKey = `${sYear}-${String(sMonth + 1).padStart(2, '0')}`; 209 210 const sArr = checkInMap[sMonthKey] || []; 211 const hasCheckIn = sArr.includes(sDay); 212 213 if (checkingToday) { 214 if (hasCheckIn) streak++; 215 checkingToday = false; 216 } else { 217 if (hasCheckIn) { 218 streak++; 219 } else { 220 break; // Gặp ngà y Äứt chuá»i -> Dừng quét! 221 } 222 } 223 // Lùi 1 ngà y an toà n qua má»c thá»i gian (JavaScript tá»± lo lùi tháng/nÄm) 224 scanDate.setUTCDate(scanDate.getUTCDate() - 1); 225 } 226 227 streakEl.innerHTML = `<i class="fas fa-fire text-warning"></i> Liên tiếp: ${streak}`; 228 229 // Cáºp nháºt Nút chÃnh (dùng mảng checkInsThisMonth cho nhất quán) 230 const hasCheckedInToday = checkInsThisMonth.includes(vnDay); 231 if (hasCheckedInToday) { 232 if (btnDaily) { 233 btnDaily.classList.add("disabled-claimed"); 234 btnDaily.innerHTML = `<i class="fas fa-check-circle"></i> Äà ÄIá»M DANH HÃM NAY`; 235 } 236 } else { 237 if (btnDaily) { 238 btnDaily.classList.remove("disabled-claimed"); 239 btnDaily.innerHTML = `ÄIá»M DANH HÃM NAY <span class="badge bg-white text-dark ms-2" style="font-size: 0.9rem; vertical-align: middle;">+50 <i class="fas fa-gem text-info"></i></span>`; 240 } 241 } 242 243 // --- Cáºp nháºt Milestone Progress Bar --- 244 // [Và Lá»I CALENDAR: TASK 2] Lấy tá»ng tÃch lÅ©y toà n há» thá»ng ká» từ khi tạo nick (Không reset khi sang tháng) 245 let totalCheckedIn = currentUserDoc?.total_checkins; 246 if (totalCheckedIn === undefined) { 247 // Migration dá»± phòng trên client nếu Cloud Function chưa ká»p cáºp nháºt hoặc data cÅ© 248 totalCheckedIn = 0; 249 if (currentUserDoc?.checkIns) { 250 for (const m in currentUserDoc.checkIns) { 251 if (Array.isArray(currentUserDoc.checkIns[m])) { 252 totalCheckedIn += currentUserDoc.checkIns[m].length; 253 } 254 } 255 } else { 256 totalCheckedIn = checkInsThisMonth.length; 257 } 258 } 259 260 const claimedMilestones = currentUserDoc?.claimedMilestones || []; 261 renderMilestones(totalCheckedIn, claimedMilestones); 262 263 const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); 264 265 let html = ""; 266 267 for (let d = 1; d <= daysInMonth; d++) { 268 let cellClass = "day-orb"; 269 let onClick = ""; 270 271 let dateObj = new Date(Date.UTC(year, month, d)); 272 let dayOfWeek = dateObj.getUTCDay(); 273 let dayStr = dayOfWeek === 0 ? "CN" : `T${dayOfWeek + 1}`; 274 let innerHtml = `<span style="font-size: 0.65rem; color: #888; font-weight: normal; margin-bottom: 2px;">${dayStr}</span><span style="line-height: 1;">${d}</span>`;
275 276 const isCheckedIn = checkInsThisMonth.includes(d); 277 278 if (d < vnDay) { 279 if (isCheckedIn) { 280 cellClass += " claimed"; 281 } else { 282 cellClass += " missed"; 283 onClick = `onclick="window.YTT.Calendar.doMakeupCheckIn(${d})"`; 284 innerHtml += `<br><i class="fas fa-gem" style="font-size: 0.65rem; color: #00bcd4; margin-top: 2px; filter: drop-shadow(0 0 2px #00bcd4)"></i>`; 285 } 286 } else if (d === vnDay) { 287 if (isCheckedIn) { 288 cellClass += " claimed"; 289 } else { 290 cellClass += " today-uncheck"; 291 onClick = `onclick="window.YTT.Calendar.doDailyCheckIn()"`; 292 } 293 } else { 294 cellClass += " future"; 295 } 296 297 html += `<div class="${cellClass}" data-day="${d}" ${onClick}>${innerHtml}</div>`; 298 } 299 container.innerHTML = html; 300 301 const todayOrb = container.querySelector(`.today-uncheck, .claimed[data-day="${vnDay}"]`); 302 if (todayOrb) { 303 todayOrb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); 304 } else { 305 const lastClaimed = container.querySelectorAll('.claimed'); 306 if (lastClaimed.length) lastClaimed[lastClaimed.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); 307 } 308 }; 309 310 const doDailyCheckIn = async () => { 311 if (isLoading) return; 312 isLoading = true; 313 314 // [OPTIMISTIC] Cáºp nháºt RAM + UI ngay, CF call chạy ná»n 315 const { vnToday, vnMonth, vnDay: today } = getVNTimeVars(); 316 let rollbackCheckIns = null; 317 let rollbackLastCheckIn = null; 318 if (currentUserDoc) { 319 if (!currentUserDoc.checkIns) currentUserDoc.checkIns = {}; 320 if (!currentUserDoc.checkIns[vnMonth]) currentUserDoc.checkIns[vnMonth] = []; 321 if (!currentUserDoc.checkIns[vnMonth].includes(today)) { 322 rollbackCheckIns = currentUserDoc.checkIns[vnMonth].slice(); 323 rollbackLastCheckIn = currentUserDoc.lastCheckInDate || null; 324 currentUserDoc.checkIns[vnMonth].push(today); 325 currentUserDoc.lastCheckInDate = vnToday; // optimistic â Äá» _updateStats tÃnh streak ngay 326 if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins += 1; 327 } 328 } 329 renderCalendar(); 330 // Cáºp nháºt streak + mission toast ngay (không chá» Firestore onSnapshot) 331 if (window.YDH && window.YDH.refresh) window.YDH.refresh(); 332 333 try { 334 const { getFunctions, httpsCallable, app } = await _getFnModule(); 335 const fnRegion = getFunctions(app, 'asia-southeast1'); 336 const dailyCheckInFn = httpsCallable(fnRegion, 'dailyCheckIn'); 337 await dailyCheckInFn(); 338 // Không show toast á» Äây â mission complete toast Äã hiá»n á» bưá»c optimistic trên 339 340 await fetchUserCheckIns(); 341 if (window.updateNavbarBalance) window.updateNavbarBalance(); 342 343 } catch (e) { 344 console.error(e); 345 // [ROLLBACK] Hoà n tác nếu CF báo lá»i 346 if (rollbackCheckIns !== null && currentUserDoc?.checkIns?.[vnMonth]) { 347 currentUserDoc.checkIns[vnMonth] = rollbackCheckIns; 348 currentUserDoc.lastCheckInDate = rollbackLastCheckIn; 349 if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins -= 1; 350 } 351 renderCalendar(); 352 if (window.YDH && window.YDH.refresh) window.YDH.refresh(); 353 if (window.showToast) window.showToast(e.message || "Äiá»m danh lá»i!", "error"); 354 } finally { 355 isLoading = false; 356 } 357 }; 358 359 const doMakeupCheckIn = async (day) => { 360 if (isLoading) return; 361 362 const confirmStr = `Äạo hữu có muá»n tiêu hao 50 Linh Thạch Äá» Äiá»m danh bù cho ngà y ${day} không?`; 363 const isOk = window.ytt 364 ? await window.ytt.confirm(confirmStr, { okText: 'Bù Äắp Thiếu Sót', cancelText: 'â Há»§y' }) 365 : window.confirm(confirmStr); 366 if (!isOk) return; 367 368 isLoading = true; 369 370 // [OPTIMISTIC] Cáºp nháºt RAM + UI ngay, CF call chạy ná»n 371 const { vnMonth } = getVNTimeVars(); 372 let rollbackCheckIns = null; 373 if (currentUserDoc) { 374 if (!currentUserDoc.checkIns) currentUserDoc.checkIns = {}; 375 if (!currentUserDoc.checkIns[vnMonth]) currentUserDoc.checkIns[vnMonth] = []; 376 if (!currentUserDoc.checkIns[vnMonth].includes(day)) { 377 rollbackCheckIns = currentUserDoc.checkIns[vnMonth].slice(); 378 currentUserDoc.checkIns[vnMonth].push(day); 379 if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins += 1; 380 } 381 }
382 renderCalendar(); 383 384 try { 385 const { getFunctions, httpsCallable, app } = await _getFnModule(); 386 const fnRegion = getFunctions(app, 'asia-southeast1'); 387 const makeupCheckInFn = httpsCallable(fnRegion, 'makeupCheckIn'); 388 const res = await makeupCheckInFn({ targetDay: day }); 389 if (window.showToast) window.showToast(res.data.message, "success"); 390 391 await fetchUserCheckIns(); 392 if (window.updateNavbarBalance) window.updateNavbarBalance(); 393 394 } catch (e) { 395 console.error(e); 396 // [ROLLBACK] Hoà n tác nếu CF báo lá»i (thiếu LT, Äã Äiá»m danh...) 397 if (rollbackCheckIns !== null && currentUserDoc?.checkIns?.[vnMonth]) { 398 currentUserDoc.checkIns[vnMonth] = rollbackCheckIns; 399 if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins -= 1; 400 } 401 renderCalendar(); 402 if (window.showToast) window.showToast("Lá»i: " + (e.message || "Tà i lá»±c không Äá»§!"), "error"); 403 } finally { 404 isLoading = false; 405 } 406 }; 407 408 const doClaimMilestone = async (milestoneDays) => { 409 try { 410 const { getFunctions, httpsCallable, app } = await _getFnModule(); 411 const fns = getFunctions(app, 'asia-southeast1'); 412 const claimFn = httpsCallable(fns, 'claimMilestoneReward'); 413 if (window.showToast) window.showToast('Äang xá» lý...', 'info'); 414 const result = await claimFn({ milestoneDays }); 415 if (result.data.success) { 416 if (window.showToast) window.showToast('Nháºn quà thà nh công! Váºt phẩm Äã và o túi Äá».', 'success'); 417 await fetchUserCheckIns(); 418 } 419 } catch (e) { 420 const msg = e?.message || 'Có lá»i xảy ra!'; 421 if (window.showToast) window.showToast('Lá»i: ' + msg, 'error'); 422 } 423 }; 424 425 return { 426 openModal: triggerOpenModal, 427 closeModal, 428 doDailyCheckIn, 429 doMakeupCheckIn, 430 doClaimMilestone 431 }; 432})();
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.