index.html (103860B)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 6 <title>WhatsApp Logger</title> 7 8 <!-- PWA Manifest --> 9 <link rel="manifest" href="/assets/manifest-wpChat.json"> 10 <meta name="theme-color" content="#006C4C"> 11 12 <!-- SVG Favicon --> 13 <link rel="icon" href="/assets/icon.svg"> 14 15 <!-- Fonts & Icons --> 16 <link rel="preconnect" href="https://fonts.googleapis.com"> 17 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 18 <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet"> 19 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> 20 21 <!-- Firebase SDKs --> 22 <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script> 23 <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script> 24 25 <style> 26 :root { 27 /* Light Theme Tokens */ 28 --md-sys-color-primary: #006C4C; 29 --md-sys-color-on-primary: #FFFFFF; 30 --md-sys-color-primary-container: #89F8C7; 31 --md-sys-color-on-primary-container: #002114; 32 33 --md-sys-color-secondary: #4D6357; 34 --md-sys-color-on-secondary: #FFFFFF; 35 --md-sys-color-secondary-container: #CFE9D9; 36 --md-sys-color-on-secondary-container: #092016; 37 38 --md-sys-color-surface: #FBFDF9; 39 --md-sys-color-surface-dim: #D9DBD9; 40 --md-sys-color-surface-container: #ECEEEB; 41 --md-sys-color-surface-container-high: #E6E8E5; 42 43 --md-sys-color-on-surface: #191C1A; 44 --md-sys-color-on-surface-variant: #404944; 45 --md-sys-color-outline: #707974; 46 --md-sys-color-outline-variant: #BFC9C2; 47 48 --md-sys-color-error: #BA1A1A; 49 --md-sys-color-error-container: #FFDAD6; 50 --md-sys-color-on-error-container: #410002; 51 52 /* WhatsApp Specifics */ 53 --chat-bubble-me: #E2FFEC; 54 --chat-bubble-them: #FFFFFF; 55 --chat-bg-color: #EFE7DE; 56 --chat-bg-image-opacity: 0.08; 57 --search-highlight: #ffeb3b; 58 59 /* Elevations & Shapes */ 60 --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); 61 --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); 62 63 /* Settings */ 64 --base-font-size: 15px; 65 } 66 67 /* Dark Theme Tokens */ 68 [data-theme="dark"] { 69 --md-sys-color-primary: #89F8C7; 70 --md-sys-color-on-primary: #003828; 71 --md-sys-color-primary-container: #005138; 72 --md-sys-color-on-primary-container: #89F8C7; 73 74 --md-sys-color-secondary: #BACCB3; 75 --md-sys-color-on-secondary: #25342D; 76 --md-sys-color-secondary-container: #3B4B43; 77 --md-sys-color-on-secondary-container: #D6E8D8; 78 79 --md-sys-color-surface: #111412; 80 --md-sys-color-surface-dim: #111412; 81 --md-sys-color-surface-container: #1E211F; 82 --md-sys-color-surface-container-high: #282B29; 83 84 --md-sys-color-on-surface: #E2E3DD; 85 --md-sys-color-on-surface-variant: #C2C8C5; 86 --md-sys-color-outline: #8C938F; 87 --md-sys-color-outline-variant: #424945; 88 89 --md-sys-color-error: #FFB4AB; 90 --md-sys-color-error-container: #93000A; 91 --md-sys-color-on-error-container: #FFDAD6; 92 93 --chat-bubble-me: #005C4B; 94 --chat-bubble-them: #202C33; 95 --chat-bg-color: #0B141A; 96 --chat-bg-image-opacity: 0.05; 97 --search-highlight: #6b6200; 98 } 99 100 * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; } 101 102 body { 103 font-family: 'Roboto', sans-serif; 104 background-color: var(--md-sys-color-surface); 105 color: var(--md-sys-color-on-surface); 106 height: 100vh; 107 overflow: hidden; 108 display: flex; 109 justify-content: center; 110 transition: background-color 0.3s, color 0.3s; 111 } 112 113 /* --- ANIMATIONS --- */ 114 .hidden { display: none !important; } 115 .fade-in { animation: fadeIn 0.4s cubic-bezier(0.2, 0.0, 0.0, 1.0); } 116 @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } 117 .ripple-surface { position: relative; overflow: hidden; transform: translate3d(0, 0, 0); transition: transform 0.1s; } 118 .ripple-surface:active { transform: scale(0.98); } 119 120 @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } 121 @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } 122 .animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); } 123 .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; } 124 @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } } 125 126 /* --- M3 STAGGERED ENTRANCE --- */ 127 @keyframes fadeInUp { 128 from { opacity: 0; transform: translateY(30px); } 129 to { opacity: 1; transform: translateY(0); } 130 } 131 .anim-up { opacity: 0; animation: fadeInUp 0.6s cubic-bezier(0.2, 0, 0, 1) forwards; } 132 .delay-1 { animation-delay: 0.1s; } 133 .delay-2 { animation-delay: 0.2s; } 134 .delay-3 { animation-delay: 0.3s; } 135 .delay-4 { animation-delay: 0.4s; } 136 .delay-5 { animation-delay: 0.5s; } 137 138 /* --- AUTH SCREEN (UPDATED to M3 Classic) --- */ 139 #auth-screen { 140 position: fixed; inset: 0; z-index: 999; 141 /* Static blue-green background */ 142 background: radial-gradient(circle at 0% 0%, #C1E8FB 0%, transparent 50%), 143 radial-gradient(circle at 100% 100%, #89F8C7 0%, transparent 50%), 144 #FBFDF9; 145 display: flex; flex-direction: column; align-items: center; justify-content: center; 146 padding: 24px; height: 100dvh; 147 transition: opacity 0.3s; 148 } 149 150 /* Dark Mode Override for Auth Background */ 151 [data-theme="dark"] #auth-screen { 152 background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), 153 radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), 154 #111412; 155 } 156 157 /* Updated M3 Auth Card (Classic Style) */ 158 .auth-card { 159 background: rgba(251, 253, 249, 0.7); /* Translucent */ 160 backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); 161 border-radius: 28px; 162 padding: 32px 24px; 163 width: 90%; max-width: 380px; 164 box-shadow: var(--elevation-2); 165 display: flex; flex-direction: column; gap: 20px; 166 } 167 168 [data-theme="dark"] .auth-card { 169 background: rgba(30, 33, 31, 0.7); 170 } 171 172 .brand-icon { 173 width: 64px; height: 64px; 174 display: flex; align-items: center; justify-content: center; 175 margin: 0 auto 8px; 176 } 177 .brand-icon img { width: 100%; height: 100%; object-fit: contain; } 178 179 .auth-header { text-align: center; } 180 .auth-header h1 { 181 font-family: 'Google Sans', sans-serif; 182 font-size: 28px; 183 font-weight: 400; 184 color: var(--md-sys-color-on-surface); 185 margin: 8px 0; 186 } 187 .auth-header p { 188 font-family: 'Roboto', sans-serif; 189 font-size: 14px; 190 color: var(--md-sys-color-on-surface-variant); 191 margin: 0; 192 } 193 194 /* --- LOCK SCREEN (PIN/BIO) --- */ 195 #lock-screen { 196 position: fixed; inset: 0; z-index: 2000; 197 background: var(--md-sys-color-surface); 198 display: flex; flex-direction: column; align-items: center; justify-content: space-between; 199 padding: 48px 24px; opacity: 0; visibility: hidden; transition: opacity 0.3s; 200 } 201 #lock-screen.active { opacity: 1; visibility: visible; } 202 203 .lock-header { text-align: center; margin-top: 4vh; } 204 .lock-icon { width: 64px; height: 64px; margin-bottom: 16px; color: var(--md-sys-color-primary); } 205 .lock-title { font-family: 'Google Sans'; font-size: 24px; margin-bottom: 8px; } 206 .lock-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; } 207 208 /* PIN Dots */ 209 .pin-dots { display: flex; gap: 16px; margin: 32px 0; justify-content: center; } 210 .pin-dot { 211 width: 14px; height: 14px; border-radius: 50%; 212 border: 1px solid var(--md-sys-color-outline); 213 transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); 214 } 215 .pin-dot.filled { background: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-on-surface); transform: scale(1.1); } 216 .pin-dot.error { border-color: var(--md-sys-color-error); background: var(--md-sys-color-error); } 217 218 /* Native Keypad */ 219 .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px 32px; margin-bottom: 2vh; } 220 .key-btn { 221 width: 72px; height: 72px; border-radius: 50%; border: none; background: transparent; 222 font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface); 223 display: flex; align-items: center; justify-content: center; cursor: pointer; 224 transition: background 0.2s; font-family: 'Google Sans', sans-serif; 225 } 226 .key-btn:active { background: var(--md-sys-color-surface-container-high); } 227 .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); } 228 229 .forgot-pin-btn { 230 margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none; 231 font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer; 232 } 233 234 /* --- LOCKOUT UI --- */ 235 .lockout-container { 236 display: flex; flex-direction: column; align-items: center; justify-content: center; 237 text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1; 238 } 239 .lockout-icon-bg { 240 width: 80px; height: 80px; border-radius: 50%; 241 background: var(--md-sys-color-error-container); 242 color: var(--md-sys-color-on-error-container); 243 display: flex; align-items: center; justify-content: center; 244 margin-bottom: 24px; 245 } 246 .lockout-icon { font-size: 40px; } 247 .lockout-title { font-family: 'Google Sans'; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); } 248 .lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; } 249 .lockout-timer { 250 font-family: 'Google Sans'; font-size: 56px; font-weight: 400; 251 color: var(--md-sys-color-error); font-feature-settings: "tnum"; 252 margin-bottom: 8px; 253 } 254 .lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; } 255 256 /* Utility to hide PIN/BIO when locked */ 257 .locked-mode #pinUI, 258 .locked-mode #bioUI, 259 .locked-mode .forgot-pin-btn { 260 display: none !important; 261 } 262 263 /* --- UI ELEMENTS --- */ 264 .m3-field { 265 position: relative; margin-bottom: 0; 266 background: var(--md-sys-color-surface-container-highest); 267 border-radius: 4px 4px 0 0; 268 border-bottom: 1px solid var(--md-sys-color-outline); 269 transition: background 0.3s; 270 height: 56px; 271 } 272 .m3-field:hover { 273 background: rgba(29, 25, 43, 0.08); 274 } 275 .m3-field:focus-within { 276 border-bottom: 2px solid var(--md-sys-color-primary); 277 } 278 .m3-input { 279 width: 100%; height: 100%; padding: 24px 16px 8px 16px; 280 border: none; background: transparent; outline: none; 281 font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit; 282 } 283 .m3-label { 284 position: absolute; left: 16px; top: 18px; 285 color: var(--md-sys-color-on-surface-variant); 286 font-size: 16px; pointer-events: none; transition: 0.2s cubic-bezier(0.2, 0, 0, 1); 287 } 288 .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { 289 top: 8px; font-size: 12px; color: var(--md-sys-color-primary); 290 } 291 .eye-toggle { 292 position: absolute; right: 12px; top: 50%; transform: translateY(-50%); 293 border: none; background: none; cursor: pointer; color: var(--md-sys-color-on-surface-variant); 294 padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; 295 } 296 .eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); } 297 298 .m3-btn { 299 width: 100%; height: 48px; border-radius: 24px; 300 border: none; outline: none; cursor: pointer; 301 font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 14px; 302 display: flex; align-items: center; justify-content: center; gap: 8px; 303 transition: 0.2s; position: relative; overflow: hidden; 304 } 305 .m3-btn.filled { 306 background: var(--md-sys-color-primary); 307 color: var(--md-sys-color-on-primary); 308 box-shadow: none; 309 } 310 .m3-btn.filled:hover { 311 box-shadow: var(--elevation-1); 312 } 313 .m3-btn:disabled { opacity: 0.38; cursor: not-allowed; background: var(--md-sys-color-on-surface); color: var(--md-sys-color-surface); box-shadow: none; } 314 315 /* Spinner */ 316 .spinner { 317 width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); 318 border-radius: 50%; border-top-color: white; 319 animation: spin 0.8s linear infinite; display: none; 320 } 321 .m3-btn.loading .btn-text { visibility: hidden; } 322 .m3-btn.loading .btn-icon { display: none; } 323 .m3-btn.loading .spinner { display: block; position: absolute; } 324 @keyframes spin { to { transform: rotate(360deg); } } 325 326 /* --- APP LAYOUT --- */ 327 #app-layout { 328 display: flex; width: 100%; height: 100%; max-width: 1600px; 329 background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out; 330 visibility: hidden; 331 } 332 #app-layout.visible { opacity: 1; visibility: visible; } 333 334 /* SIDEBAR */ 335 .sidebar { 336 width: 400px; display: flex; flex-direction: column; 337 border-right: 1px solid var(--md-sys-color-outline-variant); 338 background: var(--md-sys-color-surface); z-index: 10; 339 } 340 .sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; } 341 .icon-btn { 342 width: 40px; height: 40px; border-radius: 50%; border: none; background: transparent; cursor: pointer; 343 color: var(--md-sys-color-on-surface-variant); display: flex; align-items: center; justify-content: center; transition: 0.2s; 344 } 345 .icon-btn:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); } 346 347 .search-container { padding: 0 16px 12px; } 348 .search-bar { 349 background: var(--md-sys-color-surface-container-high); 350 border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px; 351 } 352 .search-bar input { 353 border: none; background: transparent; outline: none; flex: 1; margin-left: 12px; 354 font-size: 16px; color: var(--md-sys-color-on-surface); 355 } 356 .contact-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px; } 357 .contact-item { 358 display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px; 359 animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; 360 } 361 .contact-item:hover { background: rgba(128,128,128,0.05); } 362 .contact-item.active { background: var(--md-sys-color-secondary-container); } 363 .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; } 364 .avatar { 365 width: 48px; height: 48px; border-radius: 50%; 366 background: var(--md-sys-color-primary-container); 367 color: var(--md-sys-color-on-primary-container); 368 display: flex; align-items: center; justify-content: center; 369 font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px; 370 } 371 .contact-info { flex: 1; min-width: 0; } 372 .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 373 .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 374 375 /* CHAT AREA */ 376 .chat-area { 377 flex: 1; display: flex; flex-direction: column; position: relative; 378 background-color: var(--chat-bg-color); 379 transition: background-color 0.3s; 380 } 381 .chat-bg-pattern { 382 position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none; 383 background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E"); 384 } 385 .chat-header { 386 height: 64px; background: var(--md-sys-color-surface); 387 display: flex; align-items: center; padding: 0 16px; 388 border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5; 389 } 390 .chat-content { 391 flex: 1; overflow-y: auto; padding: 16px; z-index: 1; 392 display: flex; flex-direction: column; gap: 4px; 393 scroll-behavior: auto; /* Removed smooth scrolling for initial load */ 394 scroll-behavior: smooth; 395 } 396 /* Override specifically to remove default smooth behavior that causes load animation */ 397 .chat-content { 398 scroll-behavior: auto !important; 399 } 400 401 .msg-bubble { 402 max-width: 80%; padding: 8px 12px; position: relative; 403 font-size: var(--base-font-size); line-height: 1.4; 404 box-shadow: 0 1px 2px rgba(0,0,0,0.1); 405 display: flex; flex-direction: column; 406 animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; 407 } 408 .msg-in { 409 align-self: flex-start; background: var(--chat-bubble-them); 410 border-radius: 0 16px 16px 16px; color: var(--md-sys-color-on-surface); 411 } 412 .msg-out { 413 align-self: flex-end; background: var(--chat-bubble-me); 414 border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface); 415 } 416 417 /* Message Grouping - WhatsApp Style */ 418 .msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; } 419 .msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; } 420 .msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; } 421 422 .msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; } 423 .msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; } 424 .msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; } 425 426 .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; } 427 .msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; } 428 429 /* DATE DIVIDERS */ 430 .chat-date-group { 431 display: flex; flex-direction: column; gap: 4px; 432 position: relative; /* Scope for sticky */ 433 } 434 .date-divider { 435 align-self: center; margin: 16px 0; 436 background: var(--md-sys-color-surface-container-high); 437 color: var(--md-sys-color-on-surface-variant); 438 font-size: 12px; font-weight: 500; 439 padding: 6px 12px; border-radius: 12px; 440 box-shadow: 0 1px 2px rgba(0,0,0,0.1); 441 position: sticky; top: 8px; z-index: 2; 442 } 443 444 .highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; } 445 446 /* Search In Chat */ 447 .in-chat-search { 448 background: var(--md-sys-color-surface-container); 449 padding: 8px 16px; display: flex; align-items: center; gap: 10px; 450 border-bottom: 1px solid var(--md-sys-color-outline-variant); 451 animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); 452 } 453 .in-chat-search input { 454 border: none; background: transparent; outline: none; flex: 1; 455 color: var(--md-sys-color-on-surface); 456 } 457 458 /* Menu */ 459 .menu-container { position: relative; } 460 .dropdown-menu { 461 position: absolute; top: 100%; right: 0; 462 background: var(--md-sys-color-surface-container-high); 463 border-radius: 12px; padding: 8px 0; min-width: 200px; 464 display: none; flex-direction: column; z-index: 100; 465 box-shadow: 0 4px 8px rgba(0,0,0,0.2); 466 animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right; 467 } 468 .dropdown-menu.show { display: flex; } 469 .menu-item { 470 padding: 12px 16px; display: flex; align-items: center; gap: 12px; 471 color: var(--md-sys-color-on-surface); background: none; border: none; 472 font-family: inherit; font-size: 14px; cursor: pointer; text-align: left; 473 } 474 .menu-item:hover { background: rgba(128,128,128,0.1); } 475 476 /* Scroll To Bottom FAB */ 477 .scroll-fab { 478 position: fixed; bottom: 24px; right: 24px; 479 width: 40px; height: 40px; border-radius: 50%; 480 background: var(--md-sys-color-surface-container-high); 481 color: var(--md-sys-color-on-surface-variant); 482 border: 1px solid var(--md-sys-color-outline-variant); 483 box-shadow: 0 2px 6px rgba(0,0,0,0.15); 484 display: flex; align-items: center; justify-content: center; 485 cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none; 486 transition: 0.3s; 487 } 488 .scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } 489 490 /* Calendar Modal */ 491 .calendar-card { 492 background: var(--md-sys-color-surface); 493 padding: 0; border-radius: 28px; width: 320px; 494 overflow: hidden; display: flex; flex-direction: column; 495 } 496 .cal-header { 497 background: var(--md-sys-color-surface-container-high); 498 padding: 16px 24px; 499 } 500 .cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; } 501 .cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; } 502 503 .cal-grid { padding: 16px 12px 0; } 504 .cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; } 505 .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; } 506 .cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; } 507 508 .cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; } 509 .cal-date { 510 width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; 511 border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface); 512 } 513 .cal-date:hover { background: var(--md-sys-color-surface-container-high); } 514 .cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); } 515 .cal-date.empty { pointer-events: none; } 516 517 /* Settings Modal */ 518 .modal-overlay { 519 position: fixed; inset: 0; z-index: 2000; 520 background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); 521 display: flex; align-items: center; justify-content: center; 522 opacity: 0; visibility: hidden; transition: 0.3s; 523 } 524 .modal-overlay.active { opacity: 1; visibility: visible; } 525 .settings-card { 526 background: var(--md-sys-color-surface); 527 width: 90%; max-width: 600px; height: 80vh; 528 border-radius: 28px; display: flex; flex-direction: column; 529 overflow: hidden; animation: scaleIn 0.3s var(--anim-enter); 530 } 531 .settings-header { 532 padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant); 533 display: flex; justify-content: space-between; align-items: center; 534 } 535 .settings-content { flex: 1; overflow-y: auto; padding: 24px; } 536 .setting-section { margin-bottom: 32px; } 537 .setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; } 538 .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } 539 .setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; } 540 .setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); } 541 542 /* Toggle Switch */ 543 .switch { position: relative; display: inline-block; width: 50px; height: 28px; } 544 .switch input { opacity: 0; width: 0; height: 0; } 545 .slider { 546 position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; 547 background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s; 548 border: 1px solid var(--md-sys-color-outline); 549 } 550 .slider:before { 551 position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px; 552 background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s; 553 } 554 input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); } 555 input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); } 556 557 /* Accordion */ 558 .faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); } 559 .faq-question { 560 width: 100%; padding: 16px 0; text-align: left; background: none; border: none; 561 font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer; 562 display: flex; justify-content: space-between; 563 } 564 .faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; } 565 .faq-item.open .faq-answer { display: block; } 566 567 /* Background Selector */ 568 .bg-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; } 569 .bg-option { 570 height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; 571 background-size: cover; position: relative; overflow: hidden; 572 } 573 .bg-option.selected { border-color: var(--md-sys-color-primary); } 574 .bg-1 { background-color: #EFE7DE; } 575 .bg-2 { background-color: #d1d7db; } 576 .bg-3 { background-color: #e5ddd5; } 577 .bg-4 { background-color: #ffffff; } 578 579 /* Install Sheet - Hidden by default on PC */ 580 .install-sheet { display: none; } 581 582 /* Settings Install Card */ 583 .install-card { 584 background: var(--md-sys-color-primary-container); 585 color: var(--md-sys-color-on-primary-container); 586 padding: 16px; border-radius: 12px; margin-bottom: 32px; 587 display: flex; align-items: center; justify-content: space-between; 588 } 589 .install-card h4 { margin-bottom: 4px; } 590 .install-card p { font-size: 12px; } 591 .install-card .m3-btn { 592 width: auto; height: 36px; padding: 0 16px; 593 font-size: 14px; white-space: nowrap; flex-shrink: 0; 594 box-shadow: none; 595 } 596 597 @media (max-width: 768px) { 598 .sidebar { 599 width: 100%; 600 flex: 1; 601 border-right: none; 602 /* Mobile-only background gradient */ 603 background: radial-gradient(circle at 0% 0%, #c1e8fb49 0%, transparent 50%), 604 radial-gradient(circle at 100% 100%, #89f8c86b 0%, transparent 50%), 605 #fbfdf96c; 606 } 607 608 /* Dark mode override for sidebar on mobile */ 609 [data-theme="dark"] .sidebar { 610 background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), 611 radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), 612 #111412; 613 } 614 615 .chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); } 616 .chat-area.active { transform: translateX(0); } 617 .back-btn { display: flex !important; margin-right: 8px; } 618 619 /* Bottom Sheet Install Prompt - Only visible on Mobile */ 620 .install-sheet { 621 display: block; /* Enable it for mobile logic */ 622 position: fixed; bottom: 0; left: 0; right: 0; z-index: 3000; 623 background: var(--md-sys-color-surface); 624 border-top-left-radius: 28px; border-top-right-radius: 28px; 625 padding: 24px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15); 626 transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1); 627 } 628 .install-sheet.visible { transform: translateY(0); } 629 .sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } 630 .sheet-content { text-align: center; } 631 .sheet-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; } 632 } 633 .back-btn { display: none; margin-right: 8px; } 634 635 /* UTILITY MODALS STYLING FIX */ 636 .modal-card { 637 background-color: var(--md-sys-color-surface); 638 padding: 24px; 639 border-radius: 28px; 640 width: 90%; 641 max-width: 320px; 642 transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1); 643 animation: scaleIn 0.3s var(--anim-enter); 644 } 645 .modal-overlay.active .modal-card { transform: scale(1); } 646 647 .modal-card.utility { 648 width: 320px; 649 padding: 24px; 650 border-radius: 28px; 651 } 652 .modal-actions { 653 display: flex; 654 justify-content: flex-end; 655 gap: 8px; 656 margin-top: 24px; 657 } 658 .modal-actions .m3-btn { 659 width: auto; 660 min-width: 80px; 661 height: 40px; 662 padding: 0 16px; 663 } 664 665 /* Empty State Centering */ 666 .empty-state { 667 display: flex; flex-direction: column; align-items: center; justify-content: center; 668 height: 100%; width: 100%; color: var(--md-sys-color-outline); 669 text-align: center; animation: fadeIn 0.5s; 670 } 671 672 /* Skeleton */ 673 .skeleton { 674 background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 75%); 675 background-size: 200% 100%; animation: shimmer 1.5s infinite linear; border-radius: 4px; 676 } 677 @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } 678 .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; } 679 .skel-avatar { width: 48px; height: 48px; border-radius: 50%; } 680 .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; } 681 .skel-msg-group { display: flex; flex-direction: column; gap: 12px; padding: 20px; width: 100%; } 682 .skel-bubble { height: 40px; border-radius: 12px; width: 40%; margin-bottom: 8px;} 683 .skel-left { align-self: flex-start; border-bottom-left-radius: 0; } 684 .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; } 685 686 </style> 687 </head> 688 <body> 689 690 <!-- AUTH SCREEN --> 691 <div id="auth-screen"> 692 <div class="auth-card"> 693 <div style="text-align: center;" class="anim-up delay-1"> 694 <div class="brand-icon"> 695 <img src="/assets/icon.svg" alt="App Logo"> 696 </div> 697 <h1 style="font-family:'Google Sans'; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1> 698 <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p> 699 </div> 700 701 <div class="m3-field anim-up delay-2"> 702 <input type="text" id="username" class="m3-input" placeholder=" "> 703 <label class="m3-label">Username</label> 704 </div> 705 706 <div class="m3-field anim-up delay-3"> 707 <input type="password" id="password" class="m3-input" placeholder=" "> 708 <label class="m3-label">Password</label> 709 <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')"> 710 <span class="material-symbols-rounded" id="passIcon">visibility</span> 711 </button> 712 </div> 713 714 <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px;"> 715 <div style="display:flex; align-items:center; gap:8px;"> 716 <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);"> 717 <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label> 718 </div> 719 </div> 720 721 <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 8px;"> 722 <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()"> 723 <span class="btn-text">Sign In</span> 724 <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span> 725 <div class="spinner"></div> 726 </button> 727 <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p> 728 </div> 729 </div> 730 </div> 731 732 <!-- LOCK SCREEN (PIN / BIO / LOCKOUT) --> 733 <div id="lock-screen"> 734 <!-- LOCKOUT UI --> 735 <div id="lockoutUI" class="hidden lockout-container"> 736 <div class="lockout-icon-bg"> 737 <span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span> 738 </div> 739 <h2 class="lockout-title" id="lockoutTitle">Locked</h2> 740 <p class="lockout-msg" id="lockoutMsg">Too many attempts.</p> 741 <div class="lockout-timer" id="lockoutTimer">00:30</div> 742 <p class="lockout-sub" id="lockoutSub">Try again later</p> 743 <div id="permanentLockActions" class="hidden" style="margin-top:24px; display:flex; gap:12px;"> 744 <button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button> 745 </div> 746 </div> 747 748 <!-- HEADER (Hidden during lockout) --> 749 <div class="lock-header" id="lockHeader"> 750 <span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span> 751 <div class="lock-title" id="lockTitle">Enter PIN</div> 752 <div class="lock-sub" id="lockSub">Protected by App Lock</div> 753 </div> 754 755 <!-- PIN UI --> 756 <div id="pinUI"> 757 <div class="pin-dots" id="pinDots"> 758 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> 759 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> 760 </div> 761 762 <div class="keypad"> 763 <button class="key-btn ripple-surface" onclick="pressKey(1)">1</button> 764 <button class="key-btn ripple-surface" onclick="pressKey(2)">2</button> 765 <button class="key-btn ripple-surface" onclick="pressKey(3)">3</button> 766 <button class="key-btn ripple-surface" onclick="pressKey(4)">4</button> 767 <button class="key-btn ripple-surface" onclick="pressKey(5)">5</button> 768 <button class="key-btn ripple-surface" onclick="pressKey(6)">6</button> 769 <button class="key-btn ripple-surface" onclick="pressKey(7)">7</button> 770 <button class="key-btn ripple-surface" onclick="pressKey(8)">8</button> 771 <button class="key-btn ripple-surface" onclick="pressKey(9)">9</button> 772 <button class="key-btn ripple-surface" style="visibility:hidden"> </button> 773 <button class="key-btn ripple-surface" onclick="pressKey(0)">0</button> 774 <button class="key-btn ripple-surface" onclick="pressKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button> 775 </div> 776 </div> 777 778 <!-- BIO ONLY UI --> 779 <div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;"> 780 <div class="ripple-surface" onclick="triggerBiometricAuth()" style="width:96px; height:96px; border-radius:50%; background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); display:flex; align-items:center; justify-content:center; cursor:pointer;"> 781 <span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span> 782 </div> 783 <p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p> 784 </div> 785 786 <div style="text-align: center;"> 787 <button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button> 788 </div> 789 </div> 790 791 <!-- MAIN APP --> 792 <div id="app-layout"> 793 <aside class="sidebar" id="sidebar"> 794 <div class="sidebar-header"> 795 <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;"> 796 <!-- Title --> 797 <h2 style="font-family: 'Google Sans', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;"> 798 Chat Log 799 </h2> 800 801 <!-- Version (Tighter Gap) --> 802 <span style="font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;"> 803 v3.10.5 804 </span> 805 </div> 806 807 <div style="display:flex; gap: 8px;"> 808 <button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button> 809 <button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button> 810 </div> 811 </div> 812 <div class="search-container"> 813 <div class="search-bar"> 814 <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span> 815 <input type="text" id="searchInput" placeholder="Search..."> 816 </div> 817 </div> 818 <div class="contact-list" id="contactList"></div> 819 </aside> 820 821 <main class="chat-area" id="chatArea"> 822 <header class="chat-header"> 823 <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"><span class="material-symbols-rounded">arrow_back</span></button> 824 <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0;"> 825 <div class="avatar" id="headerAvatar">A</div> 826 <div style="flex:1;"> 827 <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div> 828 <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div> 829 </div> 830 <div class="menu-container"> 831 <button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button> 832 <div class="dropdown-menu" id="chatMenu"> 833 <button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button> 834 <button class="menu-item" onclick="openDateSearch()"><span class="material-symbols-rounded">calendar_month</span>Search by Date</button> 835 <button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button> 836 <button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button> 837 </div> 838 </div> 839 </div> 840 </header> 841 842 <div id="inChatSearch" class="in-chat-search hidden"> 843 <input type="text" id="msgSearchInput" placeholder="Find message..."> 844 <div id="searchCount" style="font-size:12px; margin-right:8px;"></div> 845 <button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button> 846 </div> 847 848 <div class="chat-bg-pattern"></div> 849 <div class="chat-content" id="messagesContainer"> 850 <div class="empty-state"> 851 <span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span> 852 <p>Select a conversation</p> 853 </div> 854 </div> 855 856 <!-- Scroll to Bottom FAB - Now triggers SMOOTH scroll manually --> 857 <button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')"> 858 <span class="material-symbols-rounded">arrow_downward</span> 859 </button> 860 </main> 861 </div> 862 863 <!-- SETTINGS MODAL --> 864 <div id="settingsModal" class="modal-overlay"> 865 <div class="settings-card"> 866 <div class="settings-header"> 867 <h2 style="font-family:'Google Sans'">Settings</h2> 868 <button class="icon-btn" onclick="closeModal('settingsModal')"><span class="material-symbols-rounded">close</span></button> 869 </div> 870 <div class="settings-content"> 871 872 <!-- Install App --> 873 <div id="installAppSection" class="install-card hidden"> 874 <div> 875 <h4>Install App</h4> 876 <p>Get the app for better experience</p> 877 </div> 878 <button class="m3-btn filled" onclick="installPWA()">Install</button> 879 </div> 880 881 <!-- Security (Only visible on Mobile App) --> 882 <div class="setting-section hidden" id="mobileSecuritySection"> 883 <div class="setting-title">App Security</div> 884 <div class="setting-row"> 885 <div class="setting-label"><h4>PIN Lock</h4><p>Require PIN on startup</p></div> 886 <label class="switch"><input type="checkbox" id="pinLockToggle" onchange="togglePinSetting()"><span class="slider"></span></label> 887 </div> 888 <div class="setting-row"> 889 <div class="setting-label"><h4>Fingerprint Lock</h4><p>Use biometrics</p></div> 890 <label class="switch"><input type="checkbox" id="bioLockToggle" onchange="toggleBioSetting()"><span class="slider"></span></label> 891 </div> 892 </div> 893 894 <!-- Profile --> 895 <div class="setting-section"> 896 <div class="setting-title">Profile</div> 897 <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div> 898 <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div> 899 </div> 900 901 <!-- Appearance --> 902 <div class="setting-section"> 903 <div class="setting-title">Appearance</div> 904 <div class="setting-row"> 905 <div class="setting-label"><h4>Dark Mode</h4><p>Easier on the eyes</p></div> 906 <label class="switch"><input type="checkbox" id="darkModeToggle" onchange="toggleTheme()"><span class="slider"></span></label> 907 </div> 908 <div class="setting-row"> 909 <div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div> 910 <input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)"> 911 </div> 912 913 <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div> 914 <div class="bg-selector"> 915 <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div> 916 <div class="bg-option bg-2" onclick="changeBg(2)"></div> 917 <div class="bg-option bg-3" onclick="changeBg(3)"></div> 918 <div class="bg-option bg-4" onclick="changeBg(4)"></div> 919 </div> 920 </div> 921 922 <!-- Help & Legal --> 923 <div class="setting-section"> 924 <div class="setting-title">Help & Support</div> 925 <div id="faqContainer"> 926 <!-- FAQs injected via JS --> 927 </div> 928 <div style="margin-top:24px; display:flex; gap:12px;"> 929 <button class="m3-btn filled" 930 style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" 931 onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')"> 932 View FAQ 933 </button> 934 </div> 935 <div style="margin-top:24px; display:flex; gap:12px;"> 936 <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container)" onclick="window.open('mailto:mail@amit.is-a.dev')">Email Support</button> 937 </div> 938 </div> 939 940 <div class="setting-section"> 941 <div class="setting-title">Legal</div> 942 <p style="font-size:12px; color:var(--md-sys-color-on-surface-variant); line-height:1.5;"> 943 <strong>MIT License</strong><br> 944 <strong>Security:</strong> Logs are stored strictly in your private Firebase. No external access. 945 <br>Check out this project on <a href="https://amit.is-a.dev/logger">amit.is-a.dev</a> or <a href="https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-">Github</a> 946 <br>Developed by <a href="https://github.com/notamitgamer">@notamitgamer</a> 947 </p> 948 </div> 949 950 <button class="m3-btn filled" style="background:var(--md-sys-color-error);" onclick="showLogoutModal()">Log Out</button> 951 </div> 952 </div> 953 </div> 954 955 <!-- UTILITY MODALS --> 956 <div id="logoutModal" class="modal-overlay"> 957 <div class="modal-card utility"> 958 <h3 class="modal-title">Log Out?</h3> 959 <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">Session will end.</p> 960 <div class="modal-actions"> 961 <button class="m3-btn text" onclick="closeModal('logoutModal')">Cancel</button> 962 <button class="m3-btn filled" onclick="handleLogout()">Logout</button> 963 </div> 964 </div> 965 </div> 966 967 <!-- FORGOT PIN MODAL --> 968 <div id="forgotPinModal" class="modal-overlay"> 969 <div class="modal-card utility"> 970 <h3 class="modal-title">Reset App Lock</h3> 971 <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter your server credentials to reset local security.</p> 972 973 <div class="m3-field" style="margin-top:16px;"> 974 <input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label> 975 </div> 976 <div class="m3-field"> 977 <input type="password" id="resetPassword" class="m3-input" placeholder=" "> 978 <label class="m3-label">Password</label> 979 <button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')"> 980 <span class="material-symbols-rounded" id="resetPassIcon">visibility</span> 981 </button> 982 </div> 983 984 <p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p> 985 986 <div class="modal-actions"> 987 <button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button> 988 <button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()"> 989 <span class="btn-text">Reset</span> 990 <div class="spinner"></div> 991 </button> 992 </div> 993 </div> 994 </div> 995 996 <!-- RENAME MODAL --> 997 <div id="renameModal" class="modal-overlay"> 998 <div class="modal-card utility"> 999 <h3 class="modal-title">Rename Chat</h3> 1000 <div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div> 1001 <div class="modal-actions"> 1002 <button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button> 1003 <button class="m3-btn filled" onclick="saveRename()">Save</button> 1004 </div> 1005 </div> 1006 </div> 1007 1008 <!-- NOT FOUND MODAL --> 1009 <div id="notFoundModal" class="modal-overlay"> 1010 <div class="modal-card utility"> 1011 <h3 class="modal-title">No Messages Found</h3> 1012 <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p> 1013 <div class="modal-actions"> 1014 <button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button> 1015 </div> 1016 </div> 1017 </div> 1018 1019 <!-- PIN SETUP MODAL --> 1020 <div id="pinSetupModal" class="modal-overlay"> 1021 <div class="modal-card utility" style="text-align:center;"> 1022 <h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3> 1023 <div class="pin-dots" id="setupDots" style="margin: 24px 0;"> 1024 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> 1025 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> 1026 </div> 1027 <p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p> 1028 1029 <div class="keypad" style="transform: scale(0.9); margin-bottom:0;"> 1030 <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button> 1031 <button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button> 1032 <button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button> 1033 <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button> 1034 <button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button> 1035 <button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button> 1036 <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button> 1037 <button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button> 1038 <button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button> 1039 <button class="key-btn ripple-surface" style="visibility:hidden"> </button> 1040 <button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button> 1041 <button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button> 1042 </div> 1043 <div class="modal-actions" style="justify-content:center; margin-top:8px;"> 1044 <button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button> 1045 </div> 1046 </div> 1047 </div> 1048 1049 <!-- CALENDAR MODAL --> 1050 <div id="calendarModal" class="modal-overlay"> 1051 <div class="calendar-card animate-enter"> 1052 <div class="cal-header"> 1053 <h3>Select date</h3> 1054 <h2 id="calSelectedDateStr">Tue, Oct 24</h2> 1055 </div> 1056 <div class="cal-grid"> 1057 <div class="cal-nav"> 1058 <span id="calMonthYear" style="font-weight:500">October 2023</span> 1059 <div style="display:flex; gap:12px;"> 1060 <button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button> 1061 <button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button> 1062 </div> 1063 </div> 1064 <div class="cal-days"> 1065 <span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span> 1066 <span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span> 1067 </div> 1068 <div class="cal-dates" id="calDatesGrid"></div> 1069 </div> 1070 <div class="modal-actions" style="padding: 16px 24px;"> 1071 <button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button> 1072 <button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button> 1073 </div> 1074 </div> 1075 </div> 1076 1077 <!-- PWA INSTALL SHEET (Mobile Only) --> 1078 <div id="installSheet" class="install-sheet"> 1079 <div class="sheet-header"> 1080 <h3 style="font-family:'Google Sans'; margin: 0;">Install App</h3> 1081 <button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button> 1082 </div> 1083 <div class="sheet-content"> 1084 <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;"> 1085 <img src="/assets/icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;"> 1086 </div> 1087 <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, full screen view, and enhanced security.</p> 1088 <div class="sheet-actions"> 1089 <button class="m3-btn filled" onclick="installPWA()">Install App</button> 1090 <button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button> 1091 </div> 1092 </div> 1093 </div> 1094 1095 <script> 1096 const RENDER_BACKEND_URL = "https://logger-8i35.onrender.com"; 1097 const firebaseConfig = { 1098 apiKey: "AIzaSyDqOELjpsI7nbPdK7Z6X57rVRyKUlH4bzk", 1099 authDomain: "wp-logger.firebaseapp.com", 1100 projectId: "wp-logger", 1101 storageBucket: "wp-logger.firebasestorage.app", 1102 messagingSenderId: "642661549198", 1103 appId: "1:642661549198:web:ec94fa62aea1c4e2b375df" 1104 }; 1105 1106 firebase.initializeApp(firebaseConfig); 1107 const db = firebase.firestore(); 1108 let activeChatId = null; 1109 let deferredPrompt; 1110 let currentMessages = []; 1111 1112 // --- SECURITY VARS --- 1113 let enteredPin = ""; 1114 let setupPin = ""; 1115 let setupStep = 0; 1116 let isLocked = false; 1117 // Lockout Variables 1118 let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0'); 1119 let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0'); 1120 let lockoutTimerInterval; 1121 1122 // --- CALENDAR VARS --- 1123 let calCurrentDate = new Date(); 1124 let calSelectedDate = new Date(); 1125 1126 // --- HELPERS --- 1127 const arrayBufferToBase64 = (buffer) => { 1128 let binary = ''; 1129 const bytes = new Uint8Array(buffer); 1130 const len = bytes.byteLength; 1131 for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); 1132 return window.btoa(binary); 1133 }; 1134 const base64ToArrayBuffer = (base64) => { 1135 const binary_string = window.atob(base64); 1136 const len = binary_string.length; 1137 const bytes = new Uint8Array(len); 1138 for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i); 1139 return bytes.buffer; 1140 }; 1141 1142 function formatDividerDate(ts) { 1143 const date = new Date(ts * 1000); 1144 const today = new Date(); 1145 const yesterday = new Date(); 1146 yesterday.setDate(yesterday.getDate() - 1); 1147 1148 if (date.toDateString() === today.toDateString()) return "Today"; 1149 if (date.toDateString() === yesterday.toDateString()) return "Yesterday"; 1150 return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); 1151 } 1152 1153 // --- PWA INSTALLATION --- 1154 if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) { 1155 navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => { 1156 console.warn('SW Register Fail:', err); 1157 }); 1158 } 1159 window.addEventListener('beforeinstallprompt', (e) => { 1160 e.preventDefault(); 1161 deferredPrompt = e; 1162 document.getElementById('installAppSection').classList.remove('hidden'); 1163 if (window.innerWidth <= 768) { 1164 setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000); 1165 } 1166 }); 1167 async function installPWA() { 1168 if (!deferredPrompt) return; 1169 deferredPrompt.prompt(); 1170 const { outcome } = await deferredPrompt.userChoice; 1171 if (outcome === 'accepted') { 1172 closeInstallSheet(); 1173 document.getElementById('installAppSection').classList.add('hidden'); 1174 } 1175 deferredPrompt = null; 1176 } 1177 1178 function closeInstallSheet() { 1179 document.getElementById('installSheet').classList.remove('visible'); 1180 } 1181 1182 // --- AUTH & BOOT SEQUENCE --- 1183 function isMobileApp() { 1184 const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://'); 1185 const isMobile = window.innerWidth <= 768; 1186 return isStandalone && isMobile; 1187 } 1188 1189 window.onload = () => { 1190 if (localStorage.getItem('theme') === 'dark') { 1191 document.body.setAttribute('data-theme', 'dark'); 1192 document.getElementById('darkModeToggle').checked = true; 1193 } 1194 document.getElementById('profileName').value = localStorage.getItem('profileName') || ''; 1195 document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || ''; 1196 const bg = localStorage.getItem('chatBg') || 1; 1197 changeBg(bg, false); 1198 populateFAQ(); 1199 1200 if(isMobileApp()) { 1201 document.getElementById('mobileSecuritySection').classList.remove('hidden'); 1202 1203 const hasPin = localStorage.getItem('wp_app_pin'); 1204 const useBio = localStorage.getItem('wp_app_bio') === 'true'; 1205 document.getElementById('pinLockToggle').checked = !!hasPin; 1206 document.getElementById('bioLockToggle').checked = useBio; 1207 1208 const authValid = localStorage.getItem('wp_auth_expiry') > Date.now(); 1209 1210 if (authValid && (hasPin || useBio)) { 1211 document.getElementById('auth-screen').style.display = 'none'; 1212 initLockScreen(hasPin, useBio); 1213 } else if (authValid) { 1214 document.getElementById('auth-screen').style.display = 'none'; 1215 document.getElementById('app-layout').classList.add('visible'); 1216 loadChats(); 1217 } else { 1218 document.getElementById('auth-screen').style.opacity = '1'; 1219 } 1220 } else { 1221 if(localStorage.getItem('wp_auth_expiry') > Date.now()) { 1222 document.getElementById('auth-screen').style.display = 'none'; 1223 document.getElementById('app-layout').classList.add('visible'); 1224 loadChats(); 1225 } else { 1226 document.getElementById('auth-screen').style.opacity = '1'; 1227 } 1228 } 1229 1230 // Scroll Listener for FAB 1231 document.getElementById('messagesContainer').addEventListener('scroll', function() { 1232 const fab = document.getElementById('scrollFab'); 1233 const st = this.scrollTop; 1234 const sh = this.scrollHeight; 1235 const ch = this.clientHeight; 1236 1237 // Show if we are more than 500px from the bottom 1238 if (sh - st - ch > 500) { 1239 fab.classList.add('visible'); 1240 } else { 1241 fab.classList.remove('visible'); 1242 } 1243 }); 1244 1245 // Fix for mobile keyboard covering inputs 1246 const inputs = document.querySelectorAll('input[type="text"], input[type="password"]'); 1247 inputs.forEach(input => { 1248 input.addEventListener('focus', () => { 1249 if (window.innerWidth <= 768) { 1250 setTimeout(() => { 1251 input.scrollIntoView({ behavior: 'smooth', block: 'center' }); 1252 }, 300); 1253 } 1254 }); 1255 }); 1256 }; 1257 1258 // --- LOCK SCREEN & LOCKOUT LOGIC --- 1259 function initLockScreen(hasPin, useBio) { 1260 const lockScreen = document.getElementById('lock-screen'); 1261 lockScreen.classList.add('active'); 1262 isLocked = true; 1263 enteredPin = ""; 1264 updatePinDots(0); 1265 1266 // Check Lockout State First 1267 if (checkLockoutState()) return; 1268 1269 if(hasPin) { 1270 document.getElementById('pinUI').classList.remove('hidden'); 1271 document.getElementById('bioUI').classList.add('hidden'); 1272 document.getElementById('lockTitle').innerText = "Enter PIN"; 1273 document.getElementById('lockIcon').innerText = "lock"; 1274 } else if (useBio && !hasPin) { 1275 document.getElementById('pinUI').classList.add('hidden'); 1276 document.getElementById('bioUI').classList.remove('hidden'); 1277 document.getElementById('lockTitle').innerText = "Scan Fingerprint"; 1278 document.getElementById('lockIcon').innerText = "fingerprint"; 1279 triggerBiometricAuth(); 1280 } 1281 } 1282 1283 function checkLockoutState() { 1284 const now = Date.now(); 1285 1286 // Permanent Lock 1287 if (failAttempts >= 5) { 1288 renderPermanentLock(); 1289 return true; 1290 } 1291 1292 // Temporary Timer Lock 1293 if (lockUntil > now) { 1294 startLockoutTimer(); 1295 return true; 1296 } 1297 1298 // Reset if timer expired 1299 if (lockUntil > 0 && lockUntil <= now) { 1300 lockUntil = 0; 1301 localStorage.setItem('wp_lock_until', '0'); 1302 // Don't reset failAttempts to 0 here to keep escalating. 1303 // But we need to hide lockout UI 1304 document.getElementById('lockoutUI').classList.add('hidden'); 1305 document.getElementById('lockHeader').classList.remove('hidden'); 1306 document.getElementById('lock-screen').classList.remove('locked-mode'); 1307 } 1308 1309 return false; 1310 } 1311 1312 function handleFailedAttempt() { 1313 failAttempts++; 1314 localStorage.setItem('wp_fail_attempts', failAttempts); 1315 1316 if (failAttempts === 3 || failAttempts === 4) { 1317 lockUntil = Date.now() + (30 * 1000); // 30 seconds 1318 localStorage.setItem('wp_lock_until', lockUntil); 1319 startLockoutTimer(); 1320 } else if (failAttempts >= 5) { 1321 renderPermanentLock(); 1322 } else { 1323 // Shake animation for attempt 1 and 2 1324 document.getElementById('pinDots').classList.add('shake'); 1325 updatePinDots(6, true); 1326 setTimeout(() => { 1327 document.getElementById('pinDots').classList.remove('shake'); 1328 enteredPin = ""; 1329 updatePinDots(0); 1330 }, 400); 1331 } 1332 } 1333 1334 function startLockoutTimer() { 1335 const ui = document.getElementById('lockoutUI'); 1336 const screen = document.getElementById('lock-screen'); 1337 const timerDisplay = document.getElementById('lockoutTimer'); 1338 1339 ui.classList.remove('hidden'); 1340 document.getElementById('lockHeader').classList.add('hidden'); 1341 screen.classList.add('locked-mode'); // Hides PIN/BIO inputs 1342 1343 document.getElementById('lockoutTitle').innerText = "Locked"; 1344 document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`; 1345 document.getElementById('lockoutSub').innerText = "Try again later"; 1346 document.getElementById('permanentLockActions').classList.add('hidden'); 1347 document.getElementById('lockoutTimer').classList.remove('hidden'); 1348 1349 if(lockoutTimerInterval) clearInterval(lockoutTimerInterval); 1350 1351 lockoutTimerInterval = setInterval(() => { 1352 const left = Math.ceil((lockUntil - Date.now()) / 1000); 1353 if (left <= 0) { 1354 clearInterval(lockoutTimerInterval); 1355 timerDisplay.innerText = "00:00"; 1356 // Reset View 1357 initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true'); 1358 document.getElementById('lockoutUI').classList.add('hidden'); 1359 document.getElementById('lockHeader').classList.remove('hidden'); 1360 screen.classList.remove('locked-mode'); 1361 } else { 1362 const secs = left < 10 ? `0${left}` : left; 1363 timerDisplay.innerText = `00:${secs}`; 1364 } 1365 }, 1000); 1366 } 1367 1368 function renderPermanentLock() { 1369 const ui = document.getElementById('lockoutUI'); 1370 const screen = document.getElementById('lock-screen'); 1371 1372 ui.classList.remove('hidden'); 1373 document.getElementById('lockHeader').classList.add('hidden'); 1374 screen.classList.add('locked-mode'); 1375 1376 document.getElementById('lockoutIcon').innerText = "gpp_bad"; 1377 document.getElementById('lockoutTitle').innerText = "Security Lockout"; 1378 document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked."; 1379 document.getElementById('lockoutSub').innerText = "Use Forgot Password to reset."; 1380 document.getElementById('lockoutTimer').classList.add('hidden'); 1381 document.getElementById('permanentLockActions').classList.remove('hidden'); 1382 } 1383 1384 function pressKey(num) { 1385 if(num === -1) { 1386 enteredPin = enteredPin.slice(0, -1); 1387 } else if (enteredPin.length < 6) { 1388 enteredPin += num; 1389 } 1390 updatePinDots(enteredPin.length); 1391 1392 if(enteredPin.length === 6) { 1393 verifyPin(); 1394 } 1395 } 1396 1397 function updatePinDots(count, isError=false) { 1398 const dots = document.querySelectorAll('#pinDots .pin-dot'); 1399 dots.forEach((d, i) => { 1400 if(isError) { 1401 d.classList.add('error'); 1402 setTimeout(() => d.classList.remove('error'), 400); 1403 } else { 1404 d.classList.remove('error'); 1405 if(i < count) d.classList.add('filled'); 1406 else d.classList.remove('filled'); 1407 } 1408 }); 1409 } 1410 1411 function verifyPin() { 1412 // Re-check lockout before verifying 1413 if(checkLockoutState()) return; 1414 1415 const storedPin = localStorage.getItem('wp_app_pin'); 1416 if(storedPin && enteredPin === storedPin) { 1417 // Success - Reset Fail Counters 1418 failAttempts = 0; 1419 localStorage.setItem('wp_fail_attempts', '0'); 1420 localStorage.setItem('wp_lock_until', '0'); 1421 1422 const useBio = localStorage.getItem('wp_app_bio') === 'true'; 1423 if (useBio) { 1424 document.getElementById('pinUI').classList.add('hidden'); 1425 document.getElementById('bioUI').classList.remove('hidden'); 1426 document.getElementById('lockTitle').innerText = "Verify Identity"; 1427 document.getElementById('lockIcon').innerText = "fingerprint"; 1428 triggerBiometricAuth(); 1429 } else { 1430 unlockApp(); 1431 } 1432 } else { 1433 handleFailedAttempt(); 1434 } 1435 } 1436 1437 async function triggerBiometricAuth() { 1438 if(checkLockoutState()) return; 1439 1440 if(document.getElementById('bioUI').classList.contains('hidden') === false) { 1441 document.getElementById('lockTitle').innerText = "Verify Identity"; 1442 } 1443 try { 1444 const storedId = localStorage.getItem('wp_bio_id'); 1445 const options = { 1446 publicKey: { 1447 challenge: new Uint8Array([1, 2, 3, 4]), 1448 timeout: 60000, 1449 userVerification: "required" 1450 } 1451 }; 1452 if (storedId) { 1453 options.publicKey.allowCredentials = [{ 1454 type: 'public-key', 1455 id: base64ToArrayBuffer(storedId), 1456 transports: ['internal'] 1457 }]; 1458 } 1459 const credential = await navigator.credentials.get(options); 1460 if(credential) { 1461 // Success Bio - Reset Counters 1462 failAttempts = 0; 1463 localStorage.setItem('wp_fail_attempts', '0'); 1464 localStorage.setItem('wp_lock_until', '0'); 1465 unlockApp(); 1466 } 1467 } catch (e) { 1468 console.warn("Bio failed", e); 1469 // Note: We generally don't lock on Bio failures here as OS handles retries. 1470 // But if user cancels and goes to PIN, PIN failures will trigger lock. 1471 } 1472 } 1473 1474 function unlockApp() { 1475 isLocked = false; 1476 document.getElementById('lock-screen').classList.remove('active'); 1477 document.getElementById('app-layout').classList.add('visible'); 1478 loadChats(); 1479 } 1480 1481 // --- SECURITY SETTINGS --- 1482 function togglePinSetting() { 1483 const chk = document.getElementById('pinLockToggle'); 1484 if(chk.checked) openPinSetup(); 1485 else localStorage.removeItem('wp_app_pin'); 1486 } 1487 1488 function toggleBioSetting() { 1489 const chk = document.getElementById('bioLockToggle'); 1490 if(chk.checked) registerBiometric(); 1491 else { 1492 localStorage.removeItem('wp_app_bio'); 1493 localStorage.removeItem('wp_bio_id'); 1494 } 1495 } 1496 1497 function openPinSetup() { 1498 document.getElementById('pinSetupModal').classList.add('active'); 1499 setupStep = 1; 1500 setupPin = ""; 1501 enteredPin = ""; 1502 updateSetupDots(0); 1503 document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN"; 1504 document.getElementById('pinSetupMsg').innerText = "Enter a new PIN"; 1505 } 1506 1507 function updateSetupDots(count) { 1508 const dots = document.querySelectorAll('#setupDots .pin-dot'); 1509 dots.forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled')); 1510 } 1511 1512 function pressSetupKey(num) { 1513 if(num === -1) { 1514 enteredPin = enteredPin.slice(0, -1); 1515 } else if (enteredPin.length < 6) { 1516 enteredPin += num; 1517 } 1518 updateSetupDots(enteredPin.length); 1519 1520 if(enteredPin.length === 6) { 1521 if(setupStep === 1) { 1522 setupPin = enteredPin; 1523 enteredPin = ""; 1524 setupStep = 2; 1525 setTimeout(() => { 1526 updateSetupDots(0); 1527 document.getElementById('pinSetupTitle').innerText = "Confirm PIN"; 1528 document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm"; 1529 }, 200); 1530 } else if (setupStep === 2) { 1531 if(enteredPin === setupPin) { 1532 localStorage.setItem('wp_app_pin', setupPin); 1533 closeModal('pinSetupModal'); 1534 enteredPin = ""; 1535 } else { 1536 const dots = document.getElementById('setupDots'); 1537 dots.classList.add('shake'); 1538 setTimeout(() => { 1539 dots.classList.remove('shake'); 1540 enteredPin = ""; 1541 setupPin = ""; 1542 setupStep = 1; 1543 updateSetupDots(0); 1544 document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again"; 1545 document.getElementById('pinSetupMsg').innerText = "Enter a new PIN"; 1546 }, 500); 1547 } 1548 } 1549 } 1550 } 1551 1552 function cancelPinSetup() { 1553 document.getElementById('pinLockToggle').checked = false; 1554 closeModal('pinSetupModal'); 1555 } 1556 1557 async function registerBiometric() { 1558 try { 1559 const iconPath = new URL('/assets/icon.svg', window.location.href).toString(); 1560 const credential = await navigator.credentials.create({ 1561 publicKey: { 1562 challenge: new Uint8Array([1, 2, 3, 4]), 1563 rp: { name: "WhatsApp Logger", icon: iconPath }, 1564 user: { id: new Uint8Array([1]), name: "User", displayName: "App User" }, 1565 pubKeyCredParams: [{type: "public-key", alg: -7}], 1566 authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" }, 1567 timeout: 60000 1568 } 1569 }); 1570 1571 if(credential) { 1572 const idBase64 = arrayBufferToBase64(credential.rawId); 1573 localStorage.setItem('wp_bio_id', idBase64); 1574 localStorage.setItem('wp_app_bio', 'true'); 1575 } else { throw new Error("No cred"); } 1576 } catch (e) { 1577 console.error(e); 1578 document.getElementById('bioLockToggle').checked = false; 1579 } 1580 } 1581 1582 function showForgotPin() { 1583 document.getElementById('forgotPinModal').classList.add('active'); 1584 } 1585 1586 async function attemptPinReset() { 1587 const btn = document.getElementById('resetBtn'); 1588 const err = document.getElementById('resetError'); 1589 const u = document.getElementById('resetUsername').value; 1590 const p = document.getElementById('resetPassword').value; 1591 1592 btn.classList.add('loading'); 1593 btn.disabled = true; 1594 err.style.display = 'none'; 1595 1596 try { 1597 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, { 1598 method: 'POST', 1599 headers: {'Content-Type': 'application/json'}, 1600 body: JSON.stringify({ username: u, password: p }) 1601 }); 1602 const data = await response.json(); 1603 1604 if(data.success) { 1605 // RESET EVERYTHING 1606 localStorage.removeItem('wp_app_pin'); 1607 localStorage.removeItem('wp_app_bio'); 1608 localStorage.removeItem('wp_bio_id'); 1609 localStorage.removeItem('wp_fail_attempts'); 1610 localStorage.removeItem('wp_lock_until'); 1611 failAttempts = 0; 1612 lockUntil = 0; 1613 1614 document.getElementById('pinLockToggle').checked = false; 1615 document.getElementById('bioLockToggle').checked = false; 1616 1617 // Reset UI State 1618 document.getElementById('lockoutUI').classList.add('hidden'); 1619 document.getElementById('lockHeader').classList.remove('hidden'); 1620 document.getElementById('lock-screen').classList.remove('locked-mode'); 1621 1622 closeModal('forgotPinModal'); 1623 unlockApp(); 1624 } else { throw new Error(); } 1625 } catch (e) { 1626 err.style.display = 'block'; 1627 } finally { 1628 btn.classList.remove('loading'); 1629 btn.disabled = false; 1630 } 1631 } 1632 1633 function togglePasswordVisibility(id, iconId) { 1634 const input = document.getElementById(id); 1635 const icon = document.getElementById(iconId); 1636 if (input.type === 'password') { 1637 input.type = 'text'; 1638 icon.innerText = 'visibility_off'; 1639 } else { 1640 input.type = 'password'; 1641 icon.innerText = 'visibility'; 1642 } 1643 } 1644 1645 async function attemptLogin() { 1646 const btn = document.getElementById('loginBtn'); 1647 const err = document.getElementById('authError'); 1648 btn.classList.add('loading'); 1649 btn.disabled = true; 1650 err.style.opacity = '0'; 1651 1652 try { 1653 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, { 1654 method: 'POST', 1655 headers: {'Content-Type': 'application/json'}, 1656 body: JSON.stringify({ 1657 username: document.getElementById('username').value, 1658 password: document.getElementById('password').value 1659 }) 1660 }); 1661 const data = await response.json(); 1662 if(data.success) { 1663 if(document.getElementById('rememberMe').checked) { 1664 localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000)); 1665 } 1666 revealApp(); 1667 } else throw new Error(); 1668 } catch (e) { 1669 err.style.opacity = '1'; 1670 btn.classList.remove('loading'); 1671 btn.disabled = false; 1672 } 1673 } 1674 1675 function revealApp() { 1676 document.getElementById('auth-screen').style.display = 'none'; 1677 document.getElementById('app-layout').classList.add('visible'); 1678 loadChats(); 1679 } 1680 1681 function openSettings() { document.getElementById('settingsModal').classList.add('active'); } 1682 function closeModal(id) { document.getElementById(id).classList.remove('active'); } 1683 function toggleTheme() { 1684 const isDark = document.getElementById('darkModeToggle').checked; 1685 document.body.setAttribute('data-theme', isDark ? 'dark' : 'light'); 1686 localStorage.setItem('theme', isDark ? 'dark' : 'light'); 1687 } 1688 function changeFontSize(val) { document.documentElement.style.setProperty('--base-font-size', `${val}px`); } 1689 function changeBg(id, save=true) { 1690 document.querySelectorAll('.bg-option').forEach(el => el.classList.remove('selected')); 1691 const colors = ['#EFE7DE', '#d1d7db', '#e5ddd5', '#ffffff']; 1692 document.documentElement.style.setProperty('--chat-bg-color', colors[id-1]); 1693 const option = document.querySelector(`.bg-${id}`); 1694 if(option) option.classList.add('selected'); 1695 if(save) localStorage.setItem('chatBg', id); 1696 } 1697 function saveProfile() { 1698 localStorage.setItem('profileName', document.getElementById('profileName').value); 1699 localStorage.setItem('profilePhone', document.getElementById('profilePhone').value); 1700 } 1701 function populateFAQ() { 1702 const faqs = [ 1703 {q: "Is this secure?", a: "Yes, data lives in your private Firebase. No one else can access it."}, 1704 {q: "Does it track deleted messages?", a: "Yes, once a message is received by the server, it is logged forever, even if the sender deletes it."}, 1705 {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."}, 1706 {q: "Can I reply from here?", a: "No, this is a read-only archive designed for logging and privacy, not active chatting."}, 1707 {q: "What is the 'Pinger' for?", a: "It keeps your free Render server awake so it never misses a message due to inactivity."} 1708 ]; 1709 const container = document.getElementById('faqContainer'); 1710 faqs.forEach(item => { 1711 const div = document.createElement('div'); 1712 div.className = 'faq-item'; 1713 div.innerHTML = `<button class="faq-question" onclick="this.parentElement.classList.toggle('open')">${item.q} <span class="material-symbols-rounded">expand_more</span></button><div class="faq-answer">${item.a}</div>`; 1714 container.appendChild(div); 1715 }); 1716 } 1717 1718 function toggleChatMenu(event) { 1719 if(event) event.stopPropagation(); 1720 document.getElementById('chatMenu').classList.toggle('show'); 1721 } 1722 1723 document.addEventListener('click', function(e) { 1724 const menu = document.getElementById('chatMenu'); 1725 if(menu.classList.contains('show') && !e.target.closest('.menu-container')) { 1726 menu.classList.remove('show'); 1727 } 1728 }); 1729 1730 function toggleChatSearch() { 1731 document.getElementById('inChatSearch').classList.toggle('hidden'); 1732 document.getElementById('chatMenu').classList.remove('show'); 1733 document.getElementById('msgSearchInput').focus(); 1734 } 1735 1736 document.getElementById('msgSearchInput').addEventListener('input', (e) => { 1737 const term = e.target.value.toLowerCase(); 1738 const bubbles = document.querySelectorAll('.msg-bubble'); 1739 let count = 0; 1740 document.querySelectorAll('.highlight').forEach(span => { const parent = span.parentNode; parent.innerHTML = parent.textContent; }); 1741 1742 bubbles.forEach(b => { 1743 const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)'); 1744 if(textDiv) { 1745 const originalText = textDiv.textContent; 1746 if(term && originalText.toLowerCase().includes(term)) { 1747 b.style.display = 'flex'; 1748 const regex = new RegExp(`(${term})`, 'gi'); 1749 textDiv.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>'); 1750 count++; 1751 } else { 1752 b.style.display = term ? 'none' : 'flex'; 1753 textDiv.textContent = originalText; 1754 } 1755 } 1756 }); 1757 document.getElementById('searchCount').innerText = term ? `${count} found` : ''; 1758 }); 1759 1760 async function loadChats() { 1761 const list = document.getElementById('contactList'); 1762 list.innerHTML = ''; 1763 for(let i=0; i<5; i++) { 1764 list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`; 1765 } 1766 1767 try { 1768 const snap = await db.collection('Chats').get(); 1769 const groups = {}; 1770 const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid']; 1771 1772 snap.forEach(doc => { 1773 const d = doc.data(); 1774 if (!d.lastActive || excluded.includes(doc.id)) return; 1775 const phone = d.phoneNumber || doc.id.split('@')[0]; 1776 if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] }; 1777 else { 1778 groups[phone].ids.push(doc.id); 1779 if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d}; 1780 } 1781 }); 1782 1783 const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive); 1784 1785 list.innerHTML = ''; 1786 if(chats.length === 0) { 1787 list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`; 1788 return; 1789 } 1790 1791 chats.forEach(c => { 1792 const el = document.createElement('div'); 1793 el.className = 'contact-item'; 1794 const name = c.customName || c.displayName || c.phoneNumber || c.id; 1795 const isGroup = c.id.includes('@g.us'); 1796 const avatarContent = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase(); 1797 1798 el.innerHTML = `<div class="avatar">${avatarContent}</div><div class="contact-info"><div class="contact-name">${name}</div><div class="contact-sub">${c.phoneNumber || c.id}</div></div>`; 1799 el.onclick = () => openChat(c.ids, name, el, isGroup, c.phoneNumber || c.id); 1800 list.appendChild(el); 1801 }); 1802 } catch (err) { console.error(err); } 1803 } 1804 1805 function openChat(ids, name, el, isGroup, displayId) { 1806 activeChatId = ids[0]; 1807 isCurrentChatGroup = isGroup; 1808 document.getElementById('headerName').innerText = name; 1809 document.getElementById('chatHeaderContent').style.opacity = '1'; 1810 document.getElementById('headerId').innerText = displayId; 1811 document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase(); 1812 1813 if (window.matchMedia('(max-width: 768px)').matches) { 1814 window.history.pushState({chatOpen: true}, ""); 1815 document.getElementById('sidebar').classList.add('inactive'); 1816 document.getElementById('chatArea').classList.add('active'); 1817 } 1818 1819 if(window.unsub) window.unsub.forEach(u => u()); 1820 window.unsub = []; 1821 1822 const container = document.getElementById('messagesContainer'); 1823 container.innerHTML = `<div class="skel-msg-group"><div class="skeleton skel-bubble skel-left"></div><div class="skeleton skel-bubble skel-right" style="width:60%"></div><div class="skeleton skel-bubble skel-left" style="width:30%"></div></div>`; 1824 1825 const msgs = new Map(); 1826 ids.forEach(id => { 1827 window.unsub.push(db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp').onSnapshot(snap => { 1828 snap.forEach(d => msgs.set(d.id, d.data())); 1829 renderMsgs(msgs); 1830 })); 1831 }); 1832 } 1833 1834 // --- UPDATED RENDER MESSAGES (Date Groups for Sticky Header Fix) --- 1835 function renderMsgs(map) { 1836 const container = document.getElementById('messagesContainer'); 1837 container.innerHTML = ''; 1838 currentMessages = Array.from(map.values()).sort((a,b) => a.timestamp - b.timestamp); 1839 1840 if(currentMessages.length === 0) { 1841 container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>'; 1842 return; 1843 } 1844 1845 let lastDateStr = ""; 1846 let currentGroup = null; 1847 1848 currentMessages.forEach((m, index) => { 1849 const date = new Date(m.timestamp * 1000); 1850 const dateStr = date.toDateString(); 1851 1852 // Start New Date Group if date changed 1853 if(dateStr !== lastDateStr) { 1854 // Close previous group if exists 1855 if(currentGroup) container.appendChild(currentGroup); 1856 1857 // Create new group container 1858 currentGroup = document.createElement('div'); 1859 currentGroup.className = 'chat-date-group'; 1860 1861 // Add Sticky Divider inside group 1862 const div = document.createElement('div'); 1863 div.className = 'date-divider'; 1864 div.innerText = formatDividerDate(m.timestamp); 1865 currentGroup.appendChild(div); 1866 1867 lastDateStr = dateStr; 1868 } 1869 1870 // 2. GROUPING LOGIC (Using previous logic but appending to group) 1871 const div = document.createElement('div'); 1872 div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`; 1873 1874 const nextMsg = currentMessages[index + 1]; 1875 const prevMsg = currentMessages[index - 1]; 1876 1877 const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr); 1878 const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr); 1879 1880 if (isSamePrev && isSameNext) div.classList.add('group-mid'); 1881 else if (isSamePrev) div.classList.add('group-bot'); 1882 else if (isSameNext) div.classList.add('group-top'); 1883 1884 const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev; 1885 1886 div.id = `msg-${m.timestamp}`; 1887 div.dataset.date = dateStr; 1888 1889 div.innerHTML = ` 1890 ${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''} 1891 <div>${m.text}</div> 1892 <div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div> 1893 `; 1894 1895 // Append message to current GROUP 1896 if (currentGroup) currentGroup.appendChild(div); 1897 }); 1898 1899 // Append the final group 1900 if(currentGroup) container.appendChild(currentGroup); 1901 1902 // Force scroll to bottom on every update/load (now instant) 1903 setTimeout(() => scrollToBottom('auto'), 50); 1904 } 1905 1906 // --- SCROLL TO BOTTOM --- 1907 function scrollToBottom(behavior = 'auto') { 1908 const c = document.getElementById('messagesContainer'); 1909 c.scrollTo({ top: c.scrollHeight, behavior: behavior }); 1910 } 1911 1912 // --- CALENDAR LOGIC --- 1913 function openDateSearch() { 1914 document.getElementById('chatMenu').classList.remove('show'); 1915 document.getElementById('calendarModal').classList.add('active'); 1916 renderCalendar(calCurrentDate); 1917 } 1918 1919 function renderCalendar(date) { 1920 const year = date.getFullYear(); 1921 const month = date.getMonth(); 1922 1923 // Header 1924 document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date); 1925 1926 // Grid 1927 const firstDay = new Date(year, month, 1).getDay(); 1928 const daysInMonth = new Date(year, month + 1, 0).getDate(); 1929 const grid = document.getElementById('calDatesGrid'); 1930 grid.innerHTML = ''; 1931 1932 // Empty slots for prev month 1933 for(let i=0; i<firstDay; i++) { 1934 grid.innerHTML += `<div class="cal-date empty"></div>`; 1935 } 1936 1937 // Days 1938 for(let d=1; d<=daysInMonth; d++) { 1939 const dayEl = document.createElement('div'); 1940 dayEl.className = 'cal-date ripple-surface'; 1941 dayEl.innerText = d; 1942 1943 // Check if selected 1944 if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) { 1945 dayEl.classList.add('active'); 1946 } 1947 1948 dayEl.onclick = () => selectDate(new Date(year, month, d)); 1949 grid.appendChild(dayEl); 1950 } 1951 } 1952 1953 function changeCalMonth(delta) { 1954 calCurrentDate.setMonth(calCurrentDate.getMonth() + delta); 1955 renderCalendar(calCurrentDate); 1956 } 1957 1958 function selectDate(date) { 1959 calSelectedDate = date; 1960 document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); 1961 renderCalendar(calCurrentDate); // Re-render to show active state 1962 } 1963 1964 function jumpToDate() { 1965 if(!currentMessages.length) { 1966 closeModal('calendarModal'); 1967 return; 1968 } 1969 1970 // Find first message >= selected date 1971 // Set time to 00:00:00 for comparison 1972 const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000; 1973 const targetDateStr = calSelectedDate.toDateString(); 1974 1975 // Try to find exact match or next closest 1976 const msg = currentMessages.find(m => m.timestamp >= targetTs); 1977 1978 closeModal('calendarModal'); 1979 1980 if(msg) { 1981 const el = document.getElementById(`msg-${msg.timestamp}`); 1982 if(el) { 1983 el.scrollIntoView({behavior: 'smooth', block: 'center'}); 1984 el.style.backgroundColor = 'var(--search-highlight)'; // Flash highlight 1985 setTimeout(() => el.style.backgroundColor = '', 2000); 1986 } 1987 } else { 1988 document.getElementById('notFoundModal').classList.add('active'); 1989 } 1990 } 1991 1992 function handleLogout() { 1993 localStorage.removeItem('wp_auth_expiry'); 1994 localStorage.removeItem('wp_app_pin'); 1995 localStorage.removeItem('wp_app_bio'); 1996 location.reload(); 1997 } 1998 1999 function closeChatMobile() { 2000 window.history.back(); 2001 } 2002 2003 window.addEventListener('popstate', () => { 2004 document.getElementById('chatArea').classList.remove('remove'); 2005 document.getElementById('chatArea').classList.remove('active'); 2006 }); 2007 2008 function downloadChat() { 2009 if(!currentMessages.length) return; 2010 const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n'); 2011 const blob = new Blob([text], {type:'text/plain'}); 2012 const a = document.createElement('a'); 2013 a.href = URL.createObjectURL(blob); 2014 a.download = 'chat.txt'; 2015 a.click(); 2016 } 2017 2018 function showRenameModal() { 2019 if(!activeChatId) return; 2020 document.getElementById('renameModal').classList.add('active'); 2021 document.getElementById('renameInput').value = document.getElementById('headerName').innerText; 2022 document.getElementById('chatMenu').classList.remove('show'); 2023 } 2024 2025 async function saveRename() { 2026 const name = document.getElementById('renameInput').value; 2027 if(name) { 2028 await db.collection('Chats').doc(activeChatId).set({customName: name}, {merge:true}); 2029 document.getElementById('headerName').innerText = name; 2030 closeModal('renameModal'); 2031 loadChats(); 2032 } 2033 } 2034 2035 function showLogoutModal() { 2036 document.getElementById('logoutModal').classList.add('active'); 2037 } 2038 2039 document.getElementById('searchInput').addEventListener('input', (e) => { 2040 const val = e.target.value.toLowerCase(); 2041 document.querySelectorAll('.contact-item').forEach(el => { 2042 const name = el.innerText.toLowerCase(); 2043 el.style.display = name.includes(val) ? 'flex' : 'none'; 2044 }); 2045 }); 2046 2047 function promptAddChat() { 2048 const id = prompt("Enter WhatsApp ID (e.g. 12345@s.whatsapp.net)"); 2049 if(id) { 2050 db.collection('Chats').doc(id).set({displayName:'Manual Add', id:id, lastActive: Math.floor(Date.now()/1000)}, {merge:true}); 2051 setTimeout(loadChats, 1000); 2052 } 2053 } 2054 2055 async function refreshApp() { 2056 const btn = document.querySelector('.icon-btn span[class*="refresh"]'); 2057 if(btn) btn.style.animation = 'spin 1s infinite linear'; 2058 if ('caches' in window) { 2059 try { 2060 const keys = await caches.keys(); 2061 await Promise.all(keys.map(key => caches.delete(key))); 2062 } catch(e) { console.log("Cache clear error", e); } 2063 } 2064 if ('serviceWorker' in navigator) { 2065 const registrations = await navigator.serviceWorker.getRegistrations(); 2066 for (let registration of registrations) { 2067 await registration.unregister(); 2068 } 2069 } 2070 window.location.reload(); 2071 } 2072 </script> 2073 </body> 2074 </html>