1//rythm is a dancer 2const urls = { 3 signupOnAZNudeLive: 'https://users.aznude.com/app/signupOnAZNudeLive', 4 signinToAZNudeLive: 'https://users.aznude.com/app/signinToAZNudeLive', 5 approveUpload: 'https://users.aznude.com/app/approveUpload', 6 declineUpload: 'https://users.aznude.com/app/declineUpload', 7 deleteUser: 'https://users.aznude.com/app/deleteUser', 8 disableUser: 'https://users.aznude.com/app/disableUser', 9 reenableUser: 'https://users.aznude.com/app/reenableUser', 10 onLogin: 'https://users.aznude.com/app/onLogin', 11 signupUser: 'https://users.aznude.com/app/signupUser', 12 verifyRecaptcha: 'https://users.aznude.com/app/verifyRecaptcha', 13 camOut: 'https://users.aznude.com/app/camOut', 14 declineUploadAndBanUser: 'https://users.aznude.com/app/declineUploadAndBanUser', 15 checkUsername: 'https://users.aznude.com/app/checkUsername', 16 signupWithSocial: 'https://users.aznude.com/app/signupWithSocial', 17 signinWithSocial: 'https://users.aznude.com/app/signinWithSocial', 18 19 // signupOnAZNudeLive: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/signupOnAZNudeLive', 20 // signinToAZNudeLive: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/signinToAZNudeLive', 21 // approveUpload: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/approveUpload', 22 // declineUpload: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/declineUpload', 23 // deleteUser: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/deleteUser', 24 // disableUser: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/disableUser', 25 // reenableUser: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/reenableUser', 26 // onLogin: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/onLogin', 27 // signupUser: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/signupUser', 28 // verifyRecaptcha: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/verifyRecaptcha', 29 // camOut: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/camOut', 30 // declineUploadAndBanUser: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/declineUploadAndBanUser', 31 // checkUsername: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/checkUsername', 32 // signupWithSocial: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/signupWithSocial', 33 // signinWithSocial: 'http://localhost:1597/aznude-fea6a/us-central1/main/app/signinWithSocial', 34}; 35 36const firebaseErrorMessages = { 37 "auth/wrong-password": "The password is invalid or the user does not have a password.", 38 "auth/claims-too-large": "The claims payload provided to setCustomUserClaims() exceeds the maximum allowed size of 1000 bytes.", 39 "auth/email-already-exists": "The provided email is already in use by an existing user. Each user must have a unique email.", 40 "auth/id-token-expired": "The provided Firebase ID token is expired.", 41 "auth/id-token-revoked": "The Firebase ID token has been revoked.", 42 "auth/insufficient-permission": "The credential used to initialize the Admin SDK has insufficient permission to access the requested Authentication resource. Refer to Set up a Firebase project for documentation on how to generate a credential with appropriate permissions and use it to authenticate the Admin SDKs.", 43 "auth/invalid-argument": "An invalid argument was provided to an Authentication method. The error message should contain additional information.", 44 "auth/invalid-claims": "The custom claim attributes provided to setCustomUserClaims() are invalid.", 45 "auth/invalid-creation-time": "The creation time must be a valid UTC date string.", 46 "auth/invalid-disabled-field": "The provided value for the disabled user property is invalid. It must be a boolean.", 47 "auth/invalid-display-name": "The provided value for the displayName user property is invalid. It must be a non-empty string.", 48 "auth/invalid-email-verified": "The provided value for the emailVerified user property is invalid. It must be a boolean.", 49 "auth/invalid-hash-algorithm": "The hash algorithm must match one of the strings in the list of supported algorithms.", 50 "auth/invalid-hash-block-size": "The hash block size must be a valid number.", 51 "auth/invalid-hash-derived-key-length": "The hash derived key length must be a valid number.", 52 "auth/invalid-hash-key": "The hash key must a valid byte buffer.", 53 "auth/invalid-hash-memory-cost": "The hash memory cost must be a valid number.", 54 "auth/invalid-hash-parallelization": "The hash parallelization must be a valid number.", 55 "auth/invalid-hash-rounds": "The hash rounds must be a valid number.", 56 "auth/invalid-hash-salt-separator": "The hashing algorithm salt separator field must be a valid byte buffer.", 57 "auth/invalid-id-token": "The provided ID token is not a valid Firebase ID token.", 58 "auth/invalid-last-sign-in-time": "The last sign-in time must be a valid UTC date string.", 59 "auth/invalid-page-token": "The provided next page token in listUsers() is invali
59d. It must be a valid non-empty string.", 60 "auth/invalid-password": "The provided value for the password user property is invalid. It must be a string with at least six characters.", 61 "auth/invalid-password-hash": "The password hash must be a valid byte buffer.", 62 "auth/invalid-password-salt": "The password salt must be a valid byte buffer", 63 "auth/invalid-photo-url": "The provided value for the photoURL user property is invalid. It must be a string URL.", 64 "auth/invalid-provider-data": "The providerData must be a valid array of UserInfo objects.", 65 "auth/invalid-oauth-responsetype": "Only exactly one OAuth responseType should be set to true.", 66 "auth/invalid-session-cookie-duration": "The session cookie duration must be a valid number in milliseconds between 5 minutes and 2 weeks.", 67 "auth/invalid-uid": "The provided uid must be a non-empty string with at most 128 characters.", 68 "auth/invalid-user-import": "The user record to import is invalid.", 69 "auth/maximum-user-count-exceeded": "The maximum allowed number of users to import has been exceeded.", 70 "auth/missing-hash-algorithm": "Importing users with password hashes requires that the hashing algorithm and its parameters be provided.", 71 "auth/missing-uid": "A uid identifier is required for the current operation.", 72 "auth/missing-oauth-client-secret": "The OAuth configuration client secret is required to enable OIDC code flow.", 73 "auth/phone-number-already-exists": "The provided phoneNumber is already in use by an existing user. Each user must have a unique phoneNumber.", 74 "auth/project-not-found": "No Firebase project was found for the credential used to initialize the Admin SDKs. Refer to Set up a Firebase project for documentation on how to generate a credential for your project and use it to authenticate the Admin SDKs.", 75 "auth/reserved-claims": "One or more custom user claims provided to setCustomUserClaims() are reserved. For example, OIDC specific claims such as (sub, iat, iss, exp, aud, auth_time, etc) should not be used as keys for custom claims.", 76 "auth/session-cookie-expired": "The provided Firebase session cookie is expired.", 77 "auth/session-cookie-revoked": "The Firebase session cookie has been revoked.", 78 "auth/uid-already-exists": "The provided uid is already in use by an existing user. Each user must have a unique uid.", 79 "auth/admin-restricted-operation": "This operation is restricted to administrators only.", 80 "auth/app-not-authorized": "This app, identified by the domain where it's hosted, is not authorized to use Firebase Authentication with the provided API key. Review your key configuration in the Google API console.", 81 "auth/app-not-installed": "The requested mobile application corresponding to the identifier (Android package name or iOS bundle ID) provided is not installed on this device.", 82 "auth/captcha-check-failed": "The reCAPTCHA response token provided is either invalid, expired, already used or the domain associated with it does not match the list of whitelisted domains.", 83 "auth/code-expired": "The SMS code has expired. Please re-send the verification code to try again.", 84 "auth/cordova-not-ready": "Cordova framework is not ready.", 85 "auth/cors-unsupported": "This browser is not supported.", 86 "auth/credential-already-in-use": "This email is already associated with a different user account.", 87 "auth/custom-token-mismatch": "The custom token corresponds to a different audience.", 88 "auth/requires-recent-login": "This operation is sensitive and requires recent authentication. Log in again before retrying this request.", 89 "auth/dependent-sdk-initialized-before-auth": "Another Firebase SDK was initialized and is trying to use Auth before Auth is initialized. Please be sure to call `initializeAuth` or `getAuth` before starting any other Firebase SDK.", 90 "auth/dynamic-link-not-activated": "Please activate Dynamic Links in the Firebase Console and agree to the terms and conditions.", 91 "auth/email-change-needs-verification": "Multi-factor users must al
91ways have a verified email.", 92 "auth/email-already-in-use": "The email address is already in use by another account.", 93 "auth/emulator-config-failed": "Auth instance has already been used to make a network call. Auth can no longer be configured to use the emulator. Try calling 'connectAuthEmulator()' sooner.", 94 "auth/expired-action-code": "The action code has expired.", 95 "auth/cancelled-popup-request": "This operation has been cancelled due to another conflicting popup being opened.", 96 "auth/internal-error": "An internal AuthError has occurred.", 97 "auth/invalid-app-credential": "The phone verification request contains an invalid application verifier. The reCAPTCHA token response is either invalid or expired.", 98 "auth/invalid-app-id": "The mobile app identifier is not registed for the current project.", 99 "auth/invalid-user-token": "This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user isn't for the project associated with this API key.", 100 "auth/invalid-auth-event": "An internal AuthError has occurred.", 101 "auth/invalid-verification-code": "The SMS verification code used to create the phone auth credential is invalid. Please resend the verification code sms and be sure to use the verification code provided by the user.", 102 "auth/invalid-continue-uri": "The continue URL provided in the request is invalid.", 103 "auth/invalid-cordova-configuration": "The following Cordova plugins must be installed to enable OAuth sign-in: cordova-plugin-buildinfo, cordova-universal-links-plugin, cordova-plugin-browsertab, cordova-plugin-inappbrowser and cordova-plugin-customurlscheme.", 104 "auth/invalid-custom-token": "The custom token format is incorrect. Please check the documentation.", 105 "auth/invalid-dynamic-link-domain": "The provided dynamic link domain is not configured or authorized for the current project.", 106 "auth/invalid-email": "The email address is badly formatted.", 107 "auth/invalid-emulator-scheme": "Emulator URL must start with a valid scheme (http:// or https://).", 108 "auth/invalid-api-key": "Your API key is invalid, please check you have copied it correctly.", 109 "auth/invalid-cert-hash": "The SHA-1 certificate hash provided is invalid.", 110 "auth/invalid-credential": "The supplied auth credential is malformed or has expired.", 111 "auth/invalid-message-payload": "The email template corresponding to this action contains invalid characters in its message. Please fix by going to the Auth email templates section in the Firebase Console.", 112 "auth/invalid-multi-factor-session": "The request does not contain a valid proof of first factor successful sign-in.", 113 "auth/invalid-oauth-provider": "EmailAuthProvider is not supported for this operation. This operation only supports OAuth providers.", 114 "auth/invalid-oauth-client-id": "The OAuth client ID provided is either invalid or does not match the specified API key.", 115 "auth/unauthorized-domain": "This domain is not authorized for OAuth operations for your Firebase project. Edit the list of authorized domains from the Firebase console.", 116 "auth/invalid-action-code": "The action code is invalid. This can happen if the code is malformed, expired, or has already been used.", 117 "auth/invalid-persistence-type": "The specified persistence type is invalid. It can only be local, session or none.", 118 "auth/invalid-phone-number": "The format of the phone number provided is incorrect. Please enter the phone number in a format that can be parsed into E.164 format. E.164 phone numbers are written in the format [+][country code][subscriber number including area code].", 119 "auth/invalid-provider-id": "The specified provider ID is invali
119d.", 120 "auth/invalid-recipient-email": "The email corresponding to this action failed to send as the provided recipient email address is invalid.", 121 "auth/invalid-sender": "The email template corresponding to this action contains an invalid sender email or name. Please fix by going to the Auth email templates section in the Firebase Console.", 122 "auth/invalid-verification-id": "The verification ID used to create the phone auth credential is invalid.", 123 "auth/invalid-tenant-id": "The Auth instance's tenant ID is invalid.", 124 "auth/missing-android-pkg-name": "An Android Package Name must be provided if the Android App is required to be installed.", 125 "auth/auth-domain-config-required": "Be sure to include authDomain when calling firebase.initializeApp(), by following the instructions in the Firebase console.", 126 "auth/missing-app-credential": "The phone verification request is missing an application verifier assertion. A reCAPTCHA response token needs to be provided.", 127 "auth/missing-verification-code": "The phone auth credential was created with an empty SMS verification code.", 128 "auth/missing-continue-uri": "A continue URL must be provided in the request.", 129 "auth/missing-iframe-start": "An internal AuthError has occurred.", 130 "auth/missing-ios-bundle-id": "An iOS Bundle ID must be provided if an App Store ID is provided.", 131 "auth/missing-or-invalid-nonce": "The request does not contain a valid nonce. This can occur if the SHA-256 hash of the provided raw nonce does not match the hashed nonce in the ID token payload.", 132 "auth/missing-multi-factor-info": "No second factor identifier is provided.", 133 "auth/missing-multi-factor-session": "The request is missing proof of first factor successful sign-in.", 134 "auth/missing-phone-number": "To send verification codes, provide a phone number for the recipient.", 135 "auth/missing-verification-id": "The phone auth credential was created with an empty verification ID.", 136 "auth/app-deleted": "This instance of FirebaseApp has been deleted.", 137 "auth/multi-factor-info-not-found": "The user does not have a second factor matching the identifier provided.", 138 "auth/multi-factor-auth-required": "Proof of ownership of a second factor is required to complete sign-in.", 139 "auth/account-exists-with-different-credential": "An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.", 140 "auth/network-request-failed": "A network AuthError (such as timeout, interrupted connection or unreachable host) has occurred.", 141 "auth/no-auth-event": "An internal AuthError has occurred.", 142 "auth/no-such-provider": "User was not linked to an account with the given provider.", 143 "auth/null-user": "A null user object was provided as the argument for an operation which requires a non-null user object.", 144 "auth/operation-not-allowed": "The given sign-in provider is disabled for this Firebase project. Enable it in the Firebase console, under the sign-in method tab of the Auth section.", 145 "auth/operation-not-supported-in-this-environment": "This operation is not supported in the environment this application is running on. 'location.protocol' must be http, https or chrome-extension and web storage must be enabled.", 146 "auth/popup-blocked": "Unable to establish a connection with the popup. It may have been blocked by the browser.", 147 "auth/popup-closed-by-user": "The popup has been closed by the user before finalizing the operation.", 148 "auth/provider-already-linked": "User can only be linked to one identity for the given provider.", 149 "auth/quota-exceeded": "The project's quota for this operation has been exceeded.", 150 "auth/redirect-cancelled-by-user": "The redirect operation has been cancelled by the user before finalizing.", 151 "auth/redirect-operation-pending": "A redirect sign-in operation is already pending.", 152 "auth/rejected-credential": "The request contains malformed or mismatching credentials.", 153 "auth/second-factor-already-in-use": "The second factor is already enrolled on this account.", 154 "auth/maximum-second-factor-count-exceeded": "The maximum allowed number of second factors on a user has been exceeded.", 155 "auth/tenant-id-mismatch": "The provided tenant ID does not match the Auth instance's tenant ID", 156 "auth/timeout": "The operation has timed out.", 157 "auth/user-token-expired": "The user's credential is no longer valid. The user must sign in again.", 158 "auth/too-many-requests": "We have blocked all requests from this device due to unusual activity. Try again later.",
159 "auth/unauthorized-continue-uri": "The domain of the continue URL is not whitelisted. Please whitelist the domain in the Firebase console.", 160 "auth/unsupported-first-factor": "Enrolling a second factor or signing in with a multi-factor account requires sign-in with a supported first factor.", 161 "auth/unsupported-persistence-type": "The current environment does not support the specified persistence type.", 162 "auth/unsupported-tenant-operation": "This operation is not supported in a multi-tenant context.", 163 "auth/unverified-email": "The operation requires a verified email.", 164 "auth/user-cancelled": "The user did not grant your application the permissions it requested.", 165 "auth/user-not-found": "There is no user record corresponding to this identifier. The user may have been deleted.", 166 "auth/user-disabled": "The user account has been disabled by an administrator.", 167 "auth/user-mismatch": "The supplied credentials do not correspond to the previously signed in user.", 168 "auth/weak-password": "The password must be 6 characters long or more.", 169 "auth/web-storage-unsupported": "This browser is not supported or 3rd party cookies and data may be disabled.", 170 "auth/already-initialized": "initializeAuth() has already been called with different options. To avoid this error, call initializeAuth() with the same options as when it was originally called, or call getAuth() to return the already initialized instance." 171} 172 173async function loadScript(url) { 174 return new Promise((resolve, reject) => { 175 const script = document.createElement('script'); 176 script.src = url; 177 script.onload = resolve; 178 script.onerror = reject; 179 document.head.appendChild(script); 180 }); 181} 182 183async function loadStylesheet(url) { 184 return new Promise((resolve, reject) => { 185 const link = document.createElement('link'); 186 link.rel = 'stylesheet'; 187 link.href = url; 188 link.type = 'text/css'; 189 link.onload = resolve; 190 link.onerror = reject; 191 document.head.appendChild(link); 192 }); 193} 194 195function showMessageByID(message, selector) { 196 $(selector).hide(); 197 $(selector).html(message); 198 $(selector).fadeIn(); 199} 200 201function showMessage(message) { 202 $('#alert-message').hide(); 203 $('#alert-message').html(message); 204 $('#alert-message').fadeIn(); 205 $('html, body').animate( 206 { 207 scrollTop: $('#alert-message').offset().top - 100, 208 }, 209 0, 210 ); 211} 212 213function hideMessage() { 214 $('#alert-message').html(''); 215 $('#alert-message').hide(); 216} 217 218function convertDate(timestamp) { 219 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 220 const d = new Date(timestamp); 221 return `${('00' + d.getDate()).slice(-2)} ${monthNames[d.getMonth()]} ${d.getFullYear()} ${('00' + d.getHours()).slice(-2)}:${( 222 '00' + d.getMinutes() 223 ).slice(-2)}`; 224} 225 226function convertDateDDMMYYYY(timestamp) { 227 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 228 const d = new Date(timestamp); 229 return `${('00' + d.getDate()).slice(-2)} ${monthNames[d.getMonth()]} ${d.getFullYear()}`; 230} 231 232function getQueryStringValue(key) { 233 return decodeURIComponent( 234 window.location.search.replace( 235 new RegExp('^(?:.*[&\\?]' + encodeURIComponent(key).replace(/[\.\+\*]/g, '\\$&') + '(?:\\=([^&]*))?)?.*$', 'i'), 236 '$1', 237 ), 238 ); 239} 240 241function formatBytes(bytes, decimals) { 242 if (bytes == 0) return '0 Byte'; 243 var k = 1000; 244 var dm = decimals + 1 || 3; 245 var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 246 var i = Math.floor(Math.log(bytes) / Math.log(k)); 247 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 248} 249 250function isValidDate(dateString) { 251 if (dateString === '01/01/0001') return true; 252 253 if (!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(dateString)) return false; 254 255 var parts = dateString.split('/'); 256 var day = parseInt(parts[1], 10);
257 var month = parseInt(parts[0], 10); 258 var year = parseInt(parts[2], 10); 259 260 if (year < 1000 || year > 3000 || month == 0 || month > 12) return false; 261 262 var monthLength = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 263 264 if (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)) monthLength[1] = 29; 265 266 return day > 0 && day <= monthLength[month - 1]; 267} 268 269function isImageFile(file) { 270 const extension = file.substr(file.lastIndexOf('.') + 1); 271 272 switch (extension.toLowerCase()) { 273 case 'jpg': 274 case 'png': 275 case 'jpeg': 276 case 'gif': 277 return true; 278 break; 279 default: 280 return false; 281 } 282} 283 284function sanitize_xss(string) { 285 const map = { 286 '&': '', 287 '<': '', 288 '>': '', 289 '"': '', 290 "'": '', 291 '/': '', 292 }; 293 const reg = /[&<>"'/]/gi; 294 return string.replace(reg, (match) => map[match]); 295} 296 297function getRandomString(length) { 298 var randomChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 299 var result = ''; 300 for (var i = 0; i < length; i++) { 301 result += randomChars.charAt(Math.floor(Math.random() * randomChars.length)); 302 } 303 return result; 304} 305 306function constructAZNLiveNavbarLink(url) { 307 const site = new URL(url).pathname; 308 const queryStrings = url.substring(url.indexOf('?') + 1); 309 310 return `?site=${site}&${queryStrings}`; 311} 312 313function debounce(func, wait) { 314 let timeout; 315 return function(...args) { 316 const later = () => { 317 clearTimeout(timeout); 318 func.apply(this, args); 319 }; 320 clearTimeout(timeout); 321 timeout = setTimeout(later, wait); 322 }; 323} 324 325async function loadScript(url) { 326 return new Promise((resolve, reject) => { 327 const script = document.createElement('script'); 328 script.src = url; 329 script.onload = resolve; 330 script.onerror = reject; 331 document.head.appendChild(script); 332 }); 333} 334 335async function loadStylesheet(url) { 336 return new Promise((resolve, reject) => { 337 const link = document.createElement('link'); 338 link.rel = 'stylesheet'; 339 link.href = url; 340 link.type = 'text/css'; 341 link.onload = resolve; 342 link.onerror = reject; 343 document.head.appendChild(link); 344 }); 345} 346 347const $navbar = $(".account-dropdown"); 348const $navbaroverlay = $(".account-dropdown-overlay"); 349const $navbarToggler = $(".account-navbar__toggler"); 350 351$(function() { 352 const next = encodeURIComponent(window.location.href); 353 354 $('a[href="/signin.html"]').attr('href', '/signin.html?next=' + next); 355 356 function closeNavbar() { 357 $navbarToggler.attr("data-nav-status", "closed"); 358 $navbar.removeClass("account-dropdown--show"); 359 $navbaroverlay.removeClass("account-dropdown-overlay--show"); 360 setTimeout(() => { 361 $navbar.hide(); 362 $navbaroverlay.hide(); 363 $(document.body).css("overflow", "auto"); 364 }, window.screen.width < 1024 ? 100 : 0); 365 } 366 367 function openNavbar() { 368 $navbarToggler.attr("data-nav-status", "open"); 369 $navbar.show(); 370 $navbaroverlay.show(); 371 setTimeout(() => { 372 $navbar.addClass("account-dropdown--show"); 373 $navbaroverlay.addClass("account-dropdown-overlay--show"); 374 if (window.innerWidth < 1024) { 375 $(document.body).css("overflow", "hidden"); 376 } 377 }, 50); 378 } 379 380 $(".account-dropdown__close").on('click', closeNavbar) 381 $navbarToggler.on("click", function () { 382 const $this = $(this); 383 if ($this.attr("data-nav-status") === "closed") { 384 openNavbar(); 385 } else { 386 closeNavbar(); 387 } 388 }); 389 390 function fixDropdownPosition() { 391 const $navbarTogglerDesktop = $(".account-navbar__toggler--desktop"); 392 const navbarTogglerOffset = $navbarTogglerDesktop.offset(); 393 $navbar.css({ 394 top: navbarTogglerOffset.top + $navbarTogglerDesktop.outerHeight() + 10, 395 left: navbarTogglerOffset.left - $navbar.outerWidth() + $navbarTogglerDesktop.outerWidth() - 10, 396 }); 397 if(window.innerWidth < 1024) { 398 $(document.body).css("overflow", "hidden") 399 } else { 400 $(document.body).css("overflow", "auto") 401 } 402 } 403 404 $(window).on("resize", debounce(function () { 405 if ($navbar.is(":visible")) { 406 fixDropdownPosition(); 407 } 408 }, 1)); 409 410 $(document).on("click", function(event) { 411 if (!$(event.target).closest(".account-navbar__toggler").length && !$(event.target).closest(".modal").length && !$(event.target).closest(".account-dropdown").length) { 412 if ($navbar.is(":visible")) { 413 closeNavbar() 414 } 415 } 416 }); 417 418 function toggleHistoryModal() { 419 const modalId = String(Date.now()); 420 // Signed in, the switch is the account's (AZ-157): off here is off on every 421 // device signed in to it. new-firebase-init.js provides window.azHistorySwitch. 422 const historySwitch = window.azHistorySwitch; 423 const accountWide = !!(historySwitch && historySwitch.accountWide()); 424 const offCopy = accountWide 425 ? 'This stops saving what you view on every device signed in to your account.' 426 : 'You can disable history to stop saving what you view on this device.'; 427 428 const dialog = document.createElement('dialog'); 429 dialog.className = 'report-modal'; 430 dialog.id = 'history-modal-' + modalId; 431 dialog.setAttribute('aria-labelledby', 'history-modal-title-' + modalId); 432 dialog.style.display = 'none'; 433 434 dialog.innerHTML = ` 435 <style> 436 .btn-cancel, .btn-cancel:hover { 437 background-color: #2e2e2e6b 438 } 439
440 .modal-copy { 441 font-size: 13px; 442 } 443 444 .btn-cancel, .btn-confirm { 445 padding: 7px 10px; 446 } 447 </style> 448 <div class="report-modal-viewport"> 449 <div class="report-modal-content" role="document" aria-modal="true"> 450 <header> 451 <h2 id="history-modal-title-${modalId}" style="display: none;">Disable Viewing History</h2> 452 <button type="button" class="report-close-modal" aria-label="Close modal">Ã</button> 453 </header> 454 <form class="report-form history-form" novalidate> 455 <h3 class="section-title" style="margin-top: 0;">Turn off viewing history?</h3> 456 <p class="modal-copy">${offCopy}</p> 457 <section class="checkbox-group" style="margin-top: 12px;"> 458 <input type="checkbox" id="modal-clear-history-${modalId}"> 459 <label for="modal-clear-history-${modalId}" style="margin-bottom: 0">Also clear existing history</label> 460 </section> 461 <div class="history-actions" style="margin-bottom: 0; margin-top: 30px"> 462 <button type="button" class="report-btn-submit btn-cancel">Cancel</button> 463 <button type="submit" class="report-btn-submit btn-confirm"> 464 <span>Continue</span> 465 <svg class="loading-spinner" width="16" height="16" viewBox="0 0 50 50" style="display:none; margin-left:8px; vertical-align:middle;"> 466 <circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="5" stroke-dasharray="31.4 31.4"></circle> 467 </svg> 468 </button> 469 </div> 470 <div class="history-message" id="history-error-${modalId}" role="alert" style="display:none; margin-top:10px; font-size:13px; color:red;"></div> 471 </form> 472 </div> 473 </div>`; 474 475 document.body.appendChild(dialog); 476 477 function openDialog() { 478 document.body.style.overflow = 'hidden'; 479 dialog.style.display = 'flex'; 480 dialog.showModal(); 481 } 482 483 function closeDialog() { 484 try { dialog.close(); } catch(e) {} 485 dialog.style.display = 'none'; 486 document.body.style.overflow = ''; 487 cleanup(); 488 } 489 490 function cleanup() { 491 if (dialog && dialog.parentNode) { 492 dialog.parentNode.removeChild(dialog); 493 } 494 } 495 496 function showError(msg) { 497 const el = document.getElementById('history-error-' + modalId); 498 if (!el) return; 499 el.textContent = msg; 500 el.style.display = 'block'; 501 } 502 503 function hideError() { 504 const el = document.getElementById('history-error-' + modalId); 505 if (!el) return; 506 el.textContent = ''; 507 el.style.display = 'none'; 508 } 509 510 const content = dialog.querySelector('.report-modal-content'); 511 const form = dialog.querySelector('.history-form'); 512 const cancelBtn = dialog.querySelector('.btn-cancel'); 513 const closeBtn = dialog.querySelector('.report-close-modal'); 514 const submitBtn = dialog.querySelector('.btn-confirm'); 515 const submitText = submitBtn.querySelector('span'); 516 const spinner = submitBtn.querySelector('.loading-spinner'); 517 const clearCheckbox = dialog.querySelector('#modal-clear-history-' + modalId); 518 519 dialog.addEventListener('click', function (e) { 520 if (!e.target.closest('.report-modal-content')) { 521 closeDialog(); 522 } 523 }); 524 525 cancelBtn.addEventListener('click', closeDialog); 526 closeBtn.addEventListener('click', closeDialog); 527 528 form.addEventListener('submit', async function (e) { 529 e.preventDefault(); 530 hideError(); 531 532 submitBtn.disabled = true; 533 submitText.textContent = 'Working...'; 534 spinner.style.display = 'inline'; 535 536 let turnedOff = false;
537 try { 538 // The switch before any clear. Signed in, off takes effect here 539 // even when the account cannot be reached (new-firebase-init.js 540 // keeps it pending and retries). 541 if (historySwitch) { 542 await historySwitch.set(false); 543 } else { 544 try { 545 localStorage.setItem('history-enabled', 'false'); 546 } catch (storageErr) {} 547 } 548 turnedOff = true; 549 550 if (clearCheckbox && clearCheckbox.checked) { 551 // The account first (window.azHistory): if it fails the 552 // local list stays, or it would come back from the account. 553 if (window.azHistory) { 554 await window.azHistory.clear(['videos', 'celebs', 'movies', 'stories']); 555 } 556 try { 557 localStorage.removeItem('history-videos'); 558 localStorage.removeItem('history-celebs'); 559 localStorage.removeItem('history-movies'); 560 localStorage.removeItem('history-stories'); 561 // the story pairs new-firebase-init.js keeps beside that list 562 localStorage.removeItem('history-stories-sid'); 563 } catch (innerErr) {} 564 } 565 566 try { 567 window.historyEnabled = 'false'; 568 } catch (memErr) {} 569 570 closeDialog(); 571 } catch (error) { 572 showError('Something went wrong. Please try again.'); 573 } finally { 574 submitBtn.disabled = false; 575 submitText.textContent = 'Continue'; 576 spinner.style.display = 'none'; 577 578 try { 579 const toggler = document.querySelector('.account-dropdown__history-toggler__input'); 580 if (toggler) toggler.checked = !turnedOff; 581 } catch (toggleErr) {} 582 } 583 }); 584 585 dialog.addEventListener('close', cleanup); 586 587 openDialog(); 588 } 589 590 // Set while an account write from the toggler is in flight, so a second click 591 // cannot race it. 592 let historySwitchBusy = false; 593 594 $(".account-dropdown__history-toggler").on("click", function() { 595 const input = $(".account-dropdown__history-toggler__input"); 596 if (historySwitchBusy) { 597 input.prop("checked", window.azHistorySwitch ? window.azHistorySwitch.isOn() : input.prop("checked")); 598 return; 599 } 600 const checked = input.prop("checked"); 601 if(checked) { 602 toggleHistoryModal() 603 } else { 604 input.prop("checked", true); 605 historyEnabled = "true" 606 if (window.azHistorySwitch) { 607 // Signed in, an account write: if it fails, show the switch as it is. 608 historySwitchBusy = true; 609 window.azHistorySwitch.set(true).catch(function () { 610 input.prop("checked", window.azHistorySwitch.isOn()); 611 }).finally(function () { 612 historySwitchBusy = false; 613 }); 614 } else { 615 localStorage.setItem("history-enabled", 'true'); 616 } 617 } 618 }); 619}); 620 621function getCookie(name) { 622 const nameEQ = `${name}=`; 623 return document.cookie.split(';').reduce((acc, c) => { 624 c = c.trim(); 625 return c.startsWith(nameEQ) ? c.substring(nameEQ.length) : acc; 626 }, null); 627} 628 629function setCookie(name, value, days) { 630 const date = new Date(); 631 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 632 document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/`; 633} 634 635 636/*! Magnific Popup - v1.1.0 - 2016-02-20 637* http://dimsemenov.com/plugins/magnific-popup/ 638* Copyright (c) 2016 Dmitry Semenov; */ 639!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):window.jQuery||window.Zepto)}(function(a){var b,c,d,e,f,g,h="Close",i="BeforeClose",j="AfterClose",k="BeforeAppend",l="MarkupParse",m="Open",n="Change",o="mfp",p="."+o,q="mfp-ready",r="mfp-removing",s="mfp-prevent-close",t=function(){},u=!!window.jQuery,v=a(window),w=function(a,c){b.ev.on(o+a+p,c)},x=function(b,c,d,e){var f=document.createElement("div");return f.className="mfp-"+b,d&&(f.innerHTML=d),e?c&&c.appendChild(f):(f=a(f),c&&f.appendTo(c)),f},y=function(c,d){b.ev.triggerHandler(o+c,d),b.st.callbacks&&(c=c.charAt(0).toLowerCase()+c.slice(1),b.st.callbacks[c]&&b.st.callbacks[c].apply(b,a.isArray(d)?d:[d]))},z=function(c){return c===g&&b.currTemplate.closeBtn||(b.currTemplate.closeBtn=a(b.st.closeMarkup.replace("%title%",b.st.tClose)),g=c),b.currTemplate.closeBtn},A=function(){a.magnificPopup.instance||(b=new t,b.init(),a.magnificPopup.instance=b)},B=function(){var a=document.createElement("p").style,b=["ms","O","Moz","Webkit"];if(void 0!==a.transition)return!0;for(;b.length;)if(b.pop()+"Transition"in a)return!0;return!1};t.prototype={constructor:t,init:function(){var c=navigator.appVersion;b.isLowIE=b.isIE8=document.all&&!document.addEventListener,b.isAndroid=/android/gi.test(c),b.isIOS=/iphone|ipad|ipod/gi.test(c),b.supportsTransition=B(),b.probablyMobile=b.isAndroid||b.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),d=a(document),b.popupsCache={}},open:function(c){var e;if(c.isObj===!1){b.items=c.items.toArray(),b.index=0;var g,h=c.items;for(e=0;e<h.length;e++)if(g=h[e],g.parsed&&(g=g.el[0]),g===c.el[0]){b.index=e;break}}else b.items=a.isArray(c.items)?c.items:[c.items],b.index=c.index||0;if(b.isOpen)return void b.updateItemHTML();b.types=[],f="",c.mainEl&&c.mainEl.length?b.ev=c.mainEl.eq(0):b.ev=d,c.key?(b.popupsCache[c.key]||(b.popupsCache[c.key]={}),b.currTemplate=b.popupsCache[c.key]):b.currTemplate={},b.st=a.extend(!0,{},a.magnificPopup.defaults,c),b.fixedContentPos="auto"===b.st.fixedContentPos?!b.probablyMobile:b.st.fixedContentPos,b.st.modal&&(b.st.closeOnContentClick=!1,b.st.closeOnBgClick=!1,b.st.showCloseBtn=!1,b.st.enableEscapeKey=!1),b.bgOverlay||(b.bgOverlay=x("bg").on("click"+p,function(){b.close()}),b.wrap=x("wrap").attr("tabindex",-1).on("click"+p,function(a){b._checkIfClose(a.target)&&b.close()}),b.container=x("container",b.wrap)),b.contentContainer=x("content"),b.st.preloader&&(b.preloader=x("preloader",b.container,b.st.tLoading));var i=a.magnificPopup.modules;for(e=0;e<i.length;e++){var j=i[e];j=j.charAt(0).toUpperCase()+j.slice(1),b["init"+j].call(b)}y("BeforeOpen"),b.st.showCloseBtn&&(b.st.closeBtnInside?(w(l,function(a,b,c,d){c.close_replaceWith=z(d.type)}),f+=" mfp-close-btn-in"):b.wrap.append(z())),b.st.alignTop&&(f+=" mfp-align-top"),b.fixedContentPos?b.wrap.css({overflow:b.st.overflowY,overflowX:"hidden",overflowY:b.
639st.overflowY}):b.wrap.css({top:v.scrollTop(),position:"absolute"}),(b.st.fixedBgPos===!1||"auto"===b.st.fixedBgPos&&!b.fixedContentPos)&&b.bgOverlay.css({height:d.height(),position:"absolute"}),b.st.enableEscapeKey&&d.on("keyup"+p,function(a){27===a.keyCode&&b.close()}),v.on("resize"+p,function(){b.updateSize()}),b.st.closeOnContentClick||(f+=" mfp-auto-cursor"),f&&b.wrap.addClass(f);var k=b.wH=v.height(),n={};if(b.fixedContentPos&&b._hasScrollBar(k)){var o=b._getScrollbarSize();o&&(n.marginRight=o)}b.fixedContentPos&&(b.isIE7?a("body, html").css("overflow","hidden"):n.overflow="hidden");var r=b.st.mainClass;return b.isIE7&&(r+=" mfp-ie7"),r&&b._addClassToMFP(r),b.updateItemHTML(),y("BuildControls"),a("html").css(n),b.bgOverlay.add(b.wrap).prependTo(b.st.prependTo||a(document.body)),b._lastFocusedEl=document.activeElement,setTimeout(function(){b.content?(b._addClassToMFP(q),b._setFocus()):b.bgOverlay.addClass(q),d.on("focusin"+p,b._onFocusIn)},16),b.isOpen=!0,b.updateSize(k),y(m),c},close:function(){b.isOpen&&(y(i),b.isOpen=!1,b.st.removalDelay&&!b.isLowIE&&b.supportsTransition?(b._addClassToMFP(r),setTimeout(function(){b._close()},b.st.removalDelay)):b._close())},_close:function(){y(h);var c=r+" "+q+" ";if(b.bgOverlay.detach(),b.wrap.detach(),b.container.empty(),b.st.mainClass&&(c+=b.st.mainClass+" "),b._removeClassFromMFP(c),b.fixedContentPos){var e={marginRight:""};b.isIE7?a("body, html").css("overflow",""):e.overflow="",a("html").css(e)}d.off("keyup"+p+" focusin"+p),b.ev.off(p),b.wrap.attr("class","mfp-wrap").removeAttr("style"),b.bgOverlay.attr("class","mfp-bg"),b.container.attr("class","mfp-container"),!b.st.showCloseBtn||b.st.closeBtnInside&&b.currTemplate[b.currItem.type]!==!0||b.currTemplate.closeBtn&&b.currTemplate.closeBtn.detach(),b.st.autoFocusLast&&b._lastFocusedEl&&a(b._lastFocusedEl).focus(),b.currItem=null,b.content=null,b.currTemplate=null,b.prevHeight=0,y(j)},updateSize:function(a){if(b.isIOS){var c=document.documentElement.clientWidth/window.innerWidth,d=window.innerHeight*c;b.wrap.css("height",d),b.wH=d}else b.wH=a||v.height();b.fixedContentPos||b.wrap.css("height",b.wH),y("Resize")},updateItemHTML:function(){var c=b.items[b.index];b.contentContainer.detach(),b.content&&b.content.detach(),c.parsed||(c=b.parseEl(b.index));var d=c.type;if(y("BeforeChange",[b.currItem?b.currItem.type:"",d]),b.currItem=c,!b.currTemplate[d]){var f=b.st[d]?b.st[d].markup:!1;y("FirstMarkupParse",f),f?b.currTemplate[d]=a(f):b.currTemplate[d]=!0}e&&e!==c.type&&b.container.removeClass("mfp-"+e+"-holder");var g=b["get"+d.charAt(0).toUpperCase()+d.slice(1)](c,b.currTemplate[d]);b.appendContent(g,d),c.preloaded=!0,y(n,c),e=c.type,b.container.prepend(b.contentContainer),y("AfterChange")},appendContent:function(a,c){b.content=a,a?b.st.showCloseBtn&&b.st.closeBtnInside&&b.currTemplate[c]===!0?b.content.find(".mfp-close").length||b.content.append(z()):b.content=a:b.content="",y(k),b.container.addClass("mfp-"+c+"-holder"),b.contentContainer.append(b.content)},parseEl:function(c){var d,e=b.items[c];if(e.tagName?e={el:a(e)}:(d=e.type,e={data:e,src:e.src}),e.el){for(var f=b.types,g=0;g<f.length;g++)if(e.el.hasClass("mfp-"+f[g])){d=f[g];break}e.src=e.el.attr("data-mfp-src"),e.src||(e.src=e.el.attr("href"))}return e.type=d||b.st.type||"inline",e.index=c,e.parsed=!0,b.items[c]=e,y("ElementParse",e),b.items[c]},addGroup:function(a,c){var d=function(d){d.mfpEl=this,b._openClick(d,a,c)};c||(c={});var e="click.magnificPopup";c.mainEl=a,c.items?(c.isObj=!0,a.off(e).on(e,d)):(c.isObj=!1,c.delegate?a.off(e).on(e,c.delegate,d):(c.items=a,a.off(e).on(e,d)))},_openClick:function(c,d,e){var f=void 0!==e.midClick?e.midClick:a.magnificPopup.defaults.midClick;if(f||!(2===c.which||c.ctrlKey||c.metaKey||c.altKey||c.shiftKey)){var g=void 0!==e.disableOn?e.disableOn:a.magnificPopup.defaults.disableOn;if(g)if(a.isFunction(g)){if(!g.call(b))return!0}else if(v.width()<g)return!0;c.type&&(c.preventDefault(),b.isOpen&&c.stopPropagation()),e.el=a(c.mfpEl),e.delegate&&(e.items=d.find(e.delegate)),b.open(e)}},updateStatus:function(a,d){if(b.preloader){c!==a&&b.container.removeClass("mfp-s-"+c),d||"loading"!==a||(d=b.st.tLoading);var e={status:a,text:d};y("UpdateStatus",e),a=e.status,d=e.text,b.preloader.html(d),b.preloader.find("a").on("click",function(a){a.stopImmediatePropagation()}),b.container.addClass("mfp-s-"+a),c=a}},_checkIfClose:function(c){if(!a(c).hasClass(s)){var d=b.st.closeOnContentClick,e=b.st.closeOnBgClick;if(d&&e)return!0;
639if(!b.content||a(c).hasClass("mfp-close")||b.preloader&&c===b.preloader[0])return!0;if(c===b.content[0]||a.contains(b.content[0],c)){if(d)return!0}else if(e&&a.contains(document,c))return!0;return!1}},_addClassToMFP:function(a){b.bgOverlay.addClass(a),b.wrap.addClass(a)},_removeClassFromMFP:function(a){this.bgOverlay.removeClass(a),b.wrap.removeClass(a)},_hasScrollBar:function(a){return(b.isIE7?d.height():document.body.scrollHeight)>(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).focus()},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(c,d){if(void 0===d||d===!1)return!0;if(e=c.split("_"),e.length>1){var f=b.find(p+"-"+e[0]);if(f.length>0){var g=e[1];"replaceWith"===g?f[0]!==d[0]&&f.replaceWith(d):"img"===g?f.is("img")?f.attr("src",d):f.replaceWith(a("<img>").attr("src",d).attr("class",f.attr("class"))):f.attr(e[1],d)}}else b.find(p+"-"+c).html(d)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'<button title="%title%" type="button" class="mfp-close">×</button>',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("<div>");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'<a href="%url%">The content</a> could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;
639var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'<div class="mfp-figure"><div class="mfp-close"></div><figure><div class="mfp-img"></div><figcaption><div class="mfp-bottom-bar"><div class="mfp-title"></div><div class="mfp-counter"></div></div></figcaption></figure></div>',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'<a href="%url%">The image</a> could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});
639var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'<div class="mfp-iframe-scaler"><div class="mfp-close"></div><iframe class="mfp-iframe" src="//about:blank" frameborder="0" allowfullscreen></iframe></div>',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'<button title="%title%" type="button" class="mfp-arrow mfp-arrow-%dir%"></button>',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery";return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s);e.click(function(){b.prev()}),f.click(function(){b.next()}),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('<img class="mfp-img" />').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),A()}); 640 641// Universal Tooltip 642class UniversalTooltip { 643 constructor(el, opts = {}) { 644 this.el = el; 645 this.opts = opts; 646 this.getContent = typeof opts.content === 'function' 647 ? () =>
647 opts.content(this.el) 648 : () => opts.content ?? el.dataset.tooltip ?? ''; 649 this.trigger = opts.trigger ?? el.dataset.tooltipTrigger ?? 'hover'; 650 this.placement = opts.placement ?? el.dataset.tooltipPlacement ?? 'top'; 651 this.autoHide = opts.autoHide ?? (+el.dataset.tooltipAutohide || null); 652 this.tip = null; 653 this._show = this.show.bind(this); 654 this._hide = this.hide.bind(this); 655 this._docClick = this._onDocClick.bind(this); 656 this._init(); 657 } 658 659 static attach(selectorOrList, opts = {}) { 660 let list; 661 if (typeof selectorOrList === 'string') { 662 list = document.querySelectorAll(selectorOrList); 663 } else if (selectorOrList instanceof NodeList || Array.isArray(selectorOrList)) { 664 list = selectorOrList; 665 } else { 666 list = [selectorOrList]; 667 } 668 list.forEach(el => new UniversalTooltip(el, opts)); 669 } 670 static initAll() { UniversalTooltip.attach('[data-tooltip]'); } 671 672 _init() { 673 if (this.trigger === 'manual') return; // caller drives show()/hide(); no auto-wiring 674 if (this.trigger === 'hover') { 675 this.el.addEventListener('mouseenter', this._show); 676 this.el.addEventListener('mouseleave', this._hide); 677 } else if (this.trigger === 'click') { 678 this.el.addEventListener('click', e => { 679 e.preventDefault(); 680 this.toggle(); 681 }); 682 document.addEventListener('click', this._docClick); 683 } 684 } 685 _onDocClick(e) { 686 if (!this.el.contains(e.target) && this.tip && !this.tip.contains(e.target)) { 687 this.hide(); 688 } 689 } 690 691 _create() { 692 if (this.tip) return; 693 694 const attrCont = this.el.dataset.tooltipContainer; 695 const inDialog = this.el.closest('dialog[open]'); 696 this.container = this.opts.container 697 || (attrCont && document.querySelector(attrCont)) 698 || inDialog 699 || document.body; 700 701 if (this.container !== document.body && 702 getComputedStyle(this.container).position === 'static') { 703 this.container.style.position = 'relative'; 704 } 705 if (inDialog) inDialog.style.overflow = 'visible'; 706 707 const tip = document.createElement('div'); 708 tip.className = 'tooltip-container'; 709 tip.dataset.placement = this.placement; 710 711 if (this.trigger !== 'hover') tip.classList.add('interactive'); 712 if (this.opts.tipClass) tip.classList.add(this.opts.tipClass); 713 714 tip.innerHTML = this.getContent(); 715 tip.style.position = 'absolute'; 716 tip.style.zIndex = '2147483647'; 717 718 const arrow = document.createElement('div'); 719 arrow.className = 'tooltip-arrow'; 720 tip.appendChild(arrow); 721 722 this.container.appendChild(tip); 723 this.tip = tip; 724 } 725 726 _position() { 727 if (!this.tip) return; 728 729 const elemRect = this.el.getBoundingClientRect(); 730 const margin = 8; 731 const isBody = this.container === document.body; 732 const scrollX = isBody ? (window.scrollX || window.pageXOffset) : 0; 733 const scrollY = isBody ? (window.scrollY || window.pageYOffset) : 0; 734 735 let left = elemRect.left + elemRect.width / 2 - this.tip.offsetWidth / 2 + scrollX; 736 let top; 737 738 if (this.placement === 'bottom') { 739 top = elemRect.bottom + margin + scrollY; 740 } else { 741 top = elemRect.top - this.tip.offsetHeight - margin + scrollY; 742 } 743 744 if (!isBody) { 745 const contRect = this.container.getBoundingClientRect(); 746 left -= contRect.left; 747 top -= contRect.top; 748 } 749 750 const maxW = isBody ? window.innerWidth : this.container.clientWidth; 751 left = Math.max(8, Math.min(left, maxW - this.tip.offsetWidth - 8)); 752 753 this.tip.style.left = `${left}px`; 754 this.tip.style.top = `${top}px`; 755 756 const arrow = this.tip.querySelector('.tooltip-arrow'); 757 if (arrow) { 758 const contLeft = isBody ? 0 : this.container.getBoundingClientRect().left; 759 const centerDoc = elemRect.left + elemRect.width / 2 + scrollX; 760 let arrowLeft = centerDoc - contLeft - left; 761 const aw = 12; 762 arrowLeft = Math.max(aw / 2, Math.min(arrowLeft, this.tip.offsetWidth - aw / 2)); 763 arrow.style.left = `${arrowLeft}px`; 764 arrow.style.transform = ''; 765 } 766 } 767 768 show() { 769 this._create(); 770 this._position(); 771 requestAnimationFrame(() => this.tip.classList.add('show')); 772 if (this.autoHide) { 773 clearTimeout(this._hideTimeout); 774 this._hideTimeout = setTimeout(() => this.hide(), this.autoHide); 775 } 776 } 777 778 hide() { 779 if (!this.tip) return; 780 this.tip.classList.remove('show'); 781 const end = () => { 782 if (this.tip) { 783 this.tip.remove(); 784 this.tip = null; 785 } 786 }; 787 this.tip.addEventListener('transitionend', end, {once: true}); 788 } 789 790 toggle() { 791 (this.tip && this.tip.classList.contains('show')) ? this.hide() : this.show(); 792 } 793 794 dispose() { 795 if (this.tip) this.tip.remove(); 796 this.el.removeEventListener('mouseenter', this._show); 797 this.el.removeEventListener('mouseleave', this._hide); 798 this.el.removeEventListener('click', this.toggle); 799 document.removeEventListener('click', this._docClick); 800 this.tip = null; 801 } 802} 803 804document.addEventListener('DOMContentLoaded', () => { 805 UniversalTooltip.initAll(); 806}); 807 808// Universal Modal 809class UniversalModal { 810 constructor(element, options = {}) { 811 this.element = typeof element === 'string' ? document.querySelector(element) : element; 812 this.options = { 813 backdrop: options.backdrop !== undefined ? options.backdrop : true, 814 keyboard: options.keyboard !== undefined ? options.keyboard : true, 815 focus: options.focus !== undefined ? options.focus : true, 816 show: options.show !== undefined ? options.show : false 817 }; 818 819 this.isShown = false;
820 this.backdrop = null; 821 this.scrollbarWidth = 0; 822 823 this._init(); 824 825 if (this.options.show) { 826 this.show(); 827 } 828 } 829 830 _init() { 831 const closeButtons = this.element.querySelectorAll('[data-dismiss="universal-modal"]'); 832 closeButtons.forEach(btn => { 833 btn.addEventListener('click', (e) => { 834 e.preventDefault(); 835 this.hide(); 836 }); 837 }); 838 839 if (this.options.backdrop) { 840 this.element.addEventListener('click', (e) => { 841 if (e.target === this.element) { 842 this.hide(); 843 } 844 }); 845 } 846 847 if (this.options.keyboard) { 848 document.addEventListener('keydown', (e) => { 849 if (e.key === 'Escape' && this.isShown) { 850 this.hide(); 851 } 852 }); 853 } 854 } 855 856 show() { 857 if (this.isShown) return; 858 859 this.isShown = true; 860 861 this.scrollbarWidth = this._getScrollbarWidth(); 862 863 if (this.scrollbarWidth > 0) { 864 document.body.style.paddingRight = this.scrollbarWidth + 'px'; 865 } 866 867 document.body.classList.add('universal-modal-open'); 868 869 if (this.options.backdrop) { 870 this._showBackdrop(); 871 } 872 873 this.element.style.display = 'block'; 874 this.element.setAttribute('aria-hidden', 'false'); 875 876 setTimeout(() => { 877 this.element.classList.add('show'); 878 879 if (this.options.focus) { 880 this.element.focus(); 881 } 882 }, 10); 883 884 this._triggerEvent('show'); 885 } 886 887 hide() { 888 if (!this.isShown) return; 889 890 this.isShown = false; 891 892 this.element.classList.remove('show'); 893 894 setTimeout(() => { 895 this.element.style.display = 'none'; 896 this.element.setAttribute('aria-hidden', 'true'); 897 898 if (this.backdrop) { 899 this._hideBackdrop(); 900 } 901 902 document.body.classList.remove('universal-modal-open'); 903 document.body.style.paddingRight = ''; 904 905 this._triggerEvent('hide'); 906 }, 300); 907 } 908 909 toggle() { 910 this.isShown ? this.hide() : this.show(); 911 } 912 913 _showBackdrop() { 914 this.backdrop = document.createElement('div'); 915 this.backdrop.className = 'universal-modal-backdrop fade'; 916 document.body.appendChild(this.backdrop); 917 918 setTimeout(() => { 919 this.backdrop.classList.add('show'); 920 }, 10); 921 } 922 923 _hideBackdrop() { 924 if (!this.backdrop) return; 925 926 this.backdrop.classList.remove('show'); 927 928 setTimeout(() => { 929 if (this.backdrop && this.backdrop.parentNode) { 930 this.backdrop.parentNode.removeChild(this.backdrop); 931 } 932 this.backdrop = null; 933 }, 300); 934 } 935 936 _getScrollbarWidth() { 937 const documentWidth = document.documentElement.clientWidth; 938 return Math.abs(window.innerWidth - documentWidth); 939 } 940 941 _triggerEvent(eventName) { 942 const event = new CustomEvent('universalModal.' + eventName, { 943 bubbles: true, 944 cancelable: true, 945 detail: { universalModal: this } 946 }); 947 this.element.dispatchEvent(event); 948 } 949 950 static _jQueryInterface(config) { 951 return this.each(function() { 952 let data = this._universalModalInstance; 953 const _config = typeof config === 'object' && config; 954 955 if (!data) { 956 data = new UniversalModal(this, _config); 957 this._universalModalInstance = data; 958 } 959 960 if (typeof config === 'string') { 961 if (typeof data[config] === 'undefined') { 962 throw new TypeError(`No method named "${config}"`); 963 } 964 data[config](); 965 } 966 }); 967 } 968} 969 970document.addEventListener('click', function(e) { 971 const trigger = e.target.closest('[data-toggle="universal-modal"]'); 972 if (!trigger) return; 973 974 e.preventDefault(); 975 976 const targetSelector = trigger.getAttribute('data-target') || trigger.getAttribute('href'); 977 if (!targetSelector) return; 978
979 const targetElement = document.querySelector(targetSelector); 980 if (!targetElement) return; 981 982 let universalModal = targetElement._universalModalInstance; 983 if (!universalModal) { 984 universalModal = new UniversalModal(targetElement); 985 targetElement._universalModalInstance = universalModal; 986 } 987 988 universalModal.show(); 989}); 990 991window.UniversalModal = UniversalModal; 992 993const TOKEN_TTL_REFRESH_MS = 60_000; 994const TOKEN_STORE_KEY_PREFIX = 'st-cache:'; 995 996/* 997 * Ceiling on a single /search-token mint. See TokenManager._mint for why an 998 * unbounded mint is worse than a slow one. 999 * 1000 * 1000ms is deliberately generous against what the endpoint actually costs: it 1001 * signs a token and returns, and the D1 Worker's equivalent was measured at 1002 * 272ms end to end from a residential connection. A mint that has not answered 1003 * in a full second is not slow, it is broken, and the caller is better served 1004 * by an error it can fall back on than by a wait it cannot interrupt. 1005 * 1006 * This is not per-search overhead. Tokens are cached in memory, in 1007 * sessionStorage, and shared across tabs over BroadcastChannel, so a mint 1008 * happens roughly once per host per session rather than once per query. It does 1009 * widen the theoretical worst case for a caller that walks several hosts in 1010 * turn - each host it reaches for the first time can add up to this on top of 1011 * its own request budget - but that only bites when every backend is 1012 * simultaneously unreachable, which is the case where a bounded failure is 1013 * exactly what you want. 1014 */ 1015const TOKEN_MINT_TIMEOUT_MS = 1000; 1016const IS_LOCAL = location.hostname === 'localhost' || location.hostname === '127.0.0.1'; 1017const PRIMARY_BASE = IS_LOCAL 1018 ? 'http://localhost:1597/aznude-fea6a/us-central1/main/app' 1019 : 'https://main-aq7es5tiuq-uc.a.run.app/app'; 1020 1021const D1_BASE = IS_LOCAL 1022 ? 'http://127.0.0.1:8787' 1023 : 'https://search-aznude.aznude.workers.dev'; 1024 1025/* 1026 * Base for the Neon-only Cloud Run service (`mainNeon`, declared in 1027 * functions/index.js). Declared alongside PRIMARY_BASE and D1_BASE so it is a 1028 * page global in the same way they are. 1029 * 1030 * NOTHING READS THIS YET, deliberately. It is here so the constant lands with 1031 * the service that backs it, in one reviewable change, rather than appearing 1032 * halfway through the follow-up that makes SearchPage.jsx fall back 1033 * Neon -> Turso -> D1. Until that lands, search behaviour is unchanged. 1034 * 1035 * VERIFY THE HOST before the first thing depends on it. The production value 1036 * below is derived from the naming Cloud Run already uses across this project 1037 * (`main` -> main-aq7es5tiuq-uc.a.run.app, and the same shape for every other 1038 * function), so it should be right - but it is a prediction until 1039 * `firebase deploy --only functions:mainNeon` has actually printed a URL. A 1040 * wrong host here would not error loudly; it would make the Neon tier fail 1041 * every request and silently serve everything from the tier behind it, which 1042 * looks like "Neon is slow" rather than "Neon was never reached". 1043 * 1044 * SCOPE: results only, and that is about WHICH SERVICE this constant points 1045 * at, not which engine answers. It used to say that /exp/search-suggestions 1046 * would run against Turso whatever host served it. That stopped being true on 1047 * 2026-09-16: routes/search.js now serves suggestions from Neon first and 1048 * falls back to Turso, on `main` and `mainNeon` alike, because they mount the 1049 * same Express app. So suggestions are already on Neon and do not need this 1050 * base to get there. 1051 */ 1052const NEON_BASE = IS_LOCAL 1053 ? 'http://localhost:1597/aznude-fea6a/us-central1/mainNeon/app' 1054 : 'https://mainneon-aq7es5tiuq-uc.a.run.app/app'; 1055 1056/* 1057 * Cacheable read traffic goes through the site's own hostname, where the Go 1058 * proxy behind Cloudflare forwards /app/* to Cloud Run and the edge can answer 1059 * repeats without waking it. Everything with a session in it â auth, votes, 1060 * favourites â stays on PRIMARY_BASE: az_session is scoped to the Cloud Run 1061 * host, that shared host IS the cross-domain login, and the proxy strips 1062 * cookies in both directions by design. 1063 * 1064 * Tokens are still minted on PRIMARY_BASE. A token response is one client's 1065 * token and sid and says no-store, so sending it through the edge would cache 1066 * nothing (it used to set an az_sid cookie as well; that is gone, AZ-103). 1067 * The proxy rewrites Host to the run.app hostname of whichever service it 1068 * forwards to, so a token's `aud` has to name that service. For the trending 1069 * strip that is `main`, so the PRIMARY_BASE token is right on both paths. The 1070 * proxy sends /exp/search-suggestions to mainNeon instead (AZ-69), so the 1071 * suggestions edge tier mints on NEON_BASE; see SUGGEST_TIERS. 1072 * 1073 * EDGE_READ_HOSTS is the frontend's own switch. Empty on purpose until the 1074 * pilot host has been proven end to end (x-served-from on every hostname, a 1075 * real cache HIT on a route we chose); a host is added here only after that, 1076 * one at a time. An unlisted host â beta.*, users.*, a freshly rotated 1077 * domain â keeps today's behaviour without anyone having to remember it. 1078 */ 1079const EDGE_READ_HOSTS = new Set(['azfans.com', 'www.azfans.com']); 1080 1081let edgeReadDisabled = false;
1082 1083function edgeReadBase() { 1084 if (IS_LOCAL || edgeReadDisabled) return PRIMARY_BASE; 1085 return EDGE_READ_HOSTS.has(location.hostname) ? '/app' : PRIMARY_BASE; 1086} 1087 1088/* 1089 * One failure disables the edge path for the rest of the page view. A proxy 1090 * or rule that is wrong fails the same way for every request, so retrying 1091 * per call would only double every failure. 1092 */ 1093function disableEdgeRead(err) { 1094 if (edgeReadDisabled) return; 1095 edgeReadDisabled = true; 1096 console.warn('[edge] falling back to origin for this page view:', err && err.message); 1097} 1098 1099/* 1100 * Run attempt(base) against the edge base first and the origin second. On 1101 * the origin there is nothing left to fall back to, so its failure propagates 1102 * exactly as it did before this path existed. 1103 */ 1104async function withEdgeFallback(attempt) { 1105 const base = edgeReadBase(); 1106 try { 1107 return await attempt(base); 1108 } catch (err) { 1109 // A caller-cancelled request is not an edge failure. Without this guard 1110 // an AbortError would disable the edge path for the rest of the page 1111 // view (edgeReadDisabled is shared by every edge read) and waste an 1112 // origin retry on the already-aborted signal. 1113 if (err && err.name === 'AbortError') throw err; 1114 // A token the origin rejected is not an edge failure either: disabling 1115 // would take every edge read off the edge for one expired token, and the 1116 // origin retry would be refused the same way. The trending strip never 1117 // throws AUTH_401 today; this is here for the next caller that does. 1118 // Suggestions do not come through here -- they walk their own tiers 1119 // and make the same call there (see suggestFirstTier). 1120 if (err && String(err.message).startsWith('AUTH_401')) throw err; 1121 if (base === PRIMARY_BASE) throw err; 1122 disableEdgeRead(err); 1123 return await attempt(PRIMARY_BASE); 1124 } 1125} 1126 1127/* 1128 * The trending strip's request, against whichever base it is handed. Kept 1129 * out of the closure that calls it so the request it builds can be tested 1130 * (test/frontend/edge-read.test.js) â the token travels in headers, never in 1131 * the URL, and a non-OK answer throws so withEdgeFallback can act on it. 1132 */ 1133async function fetchTrendingFrom(base, { token, sid, gender, countryCode, sexualityParam }) { 1134 const resp = await fetch(`${base}/most-searched-queries?gender=${gender}&limit=12&period=past24hours&country=${countryCode}${sexualityParam}`, { 1135 headers: { 1136 'x-st': token, 1137 'x-sid': sid 1138 }, 1139 credentials: IS_LOCAL ? 'omit' : 'include' 1140 }); 1141 if (!resp.ok) throw new Error("Failed to load trending"); 1142 return resp.json(); 1143} 1144 1145/* 1146 * The search-suggestions request, against whichever base it is handed. Same 1147 * shape and reasoning as fetchTrendingFrom: kept out of its closure so the 1148 * request it builds is testable (test/frontend/edge-read.test.js), token in 1149 * headers never the URL, non-OK throws so the tier walk can move on. The caller 1150 * builds the path (it depends on varValues), so only base + creds live here. 1151 * 1152 * No User-Agent is set here, and none may be. The search-token gate binds the 1153 * token to a hash of the raw UA (`uah`), so the browser's own header has to 1154 * reach it byte for byte; the Go proxy is verified to forward it unchanged. 1155 * 1156 * A 401 carries the gate's reason (`aud_mismatch`, `expired`, ...) in the 1157 * message. The gate does not log its 401 body and Cloud Run shows only the 1158 * status, so the walk's failure line is the one place a wrong-audience token 1159 * is distinguishable from an expired one. It still starts with AUTH_401. 1160 */ 1161async function fetchSuggestionsFrom(base, path, { token, sid, signal }) { 1162 const resp = await fetch(base.replace(/\/$/, '') + path, { 1163 method: 'GET', 1164 signal, 1165 credentials: IS_LOCAL ? 'omit' : 'include', 1166 headers: { 1167 'x-st': token, 1168 'x-sid': sid 1169 } 1170 }); 1171 if (resp.status === 401) { 1172 let reason = ''; 1173 try { reason = (await resp.json())?.error || ''; } catch {} 1174 throw new Error(reason ? 'AUTH_401 ' + reason : 'AUTH_401'); 1175 } 1176 if (!resp.ok) throw new Error('HTTP_' + resp.status); 1177 return resp.json(); 1178} 1179 1180/* 1181 * The first tier of the suggestions walk (SUGGEST_TIERS). On an edge host it 1182 * is the edge, and the edge REPLACES neon rather than going in front of it. 1183 * The site's own /app is the Go proxy, which sends this one route to mainNeon 1184 * (AZ-69), so the edge is Neon behind the Cloudflare cache: 1185 * 1186 * edge /app -> CF cache; miss -> proxy -> mainNeon Neon 1187 * turso PRIMARY_BASE Turso 1188 * d1 D1_BASE D1 1189 * 1190 * A fourth tier would ask Neon twice. Falling back from the edge to 1191 * PRIMARY_BASE, as withEdgeFallback does for the trending strip, would reach 1192 * Turso before Neon was tried. 1193 * 1194 * THE EDGE'S TOKEN IS MINTED ON NEON_BASE, NOT ON THE BASE IT CALLS. The gate 1195 * checks the token's `aud` against the Host it is served under, and the proxy 1196 * rewrites Host to mainNeon's. A PRIMARY_BASE token there is `401 1197 * aud_mismatch`, measured through azfans after AZ-69 deployed. Nothing would 1198 * look broken: the tier would cool down and Turso would answer every keystroke. 1199 * 1200 * Once the edge is off for the page view, because the walk or the trending 1201 * strip saw the proxy fail, this is plain neon again. Kept out of the walk's 1202 * closure so the choice, and above all the token base, is tested 1203 * (test/frontend/edge-read.test.js). 1204 */ 1205function suggestFirstTier() { 1206 const neon = typeof NEON_BASE !== 'undefined' ? NEON_BASE : null; 1207 const base = edgeReadBase(); 1208 return neon && base !== PRIMARY_BASE 1209 ? { name: 'edge', base, tokenBase: neon, timeout: 1200 } 1210 : { name: 'neon', base: neon, timeout: 1200 }; 1211} 1212 1213window.TokenManager = class { 1214 constructor() { 1215 this.cache = new Map(); 1216 this.inflight = new Map(); 1217 this.bc = null; 1218 try { this.bc = new BroadcastChannel('search-token'); } catch {} 1219 if (this.bc) { 1220 this.bc.onmessage = (e) => { 1221 const { host, token, exp, sid, issuer } = e.data || {}; 1222 if (!host || !token || !exp || !issuer) return; 1223 this.cache.set(host, { token, exp, sid, issuer }); 1224 try { sessionStorage.setItem(TOKEN_STORE_KEY_PREFIX + host, JSON.stringify({ token, exp, sid, issuer })); } catch {} 1225 }; 1226 } 1227 document.addEventListener('visibilitychange', () => { 1228 if (document.visibilityState === 'visible') { 1229 for (const host of this.cache.keys()) { 1230 this.get(host, false).catch(()=>{}); 1231 } 1232 } 1233 }); 1234 } 1235 1236 _loadFromSession(host) { 1237 try { 1238 const raw = sessionStorage.getItem(TOKEN_STORE_KEY_PREFIX + host); 1239 if (!raw) return null; 1240 const parsed = JSON.parse(raw); 1241 1242 if (parsed.issuer && parsed.issuer !== host) { 1243 sessionStorage.removeItem(TOKEN_STORE_KEY_PREFIX + host); 1244 return null; 1245 } 1246 1247 if (typeof parsed.exp !== 'number' || !parsed.token) return null; 1248 return parsed; 1249 } catch { return null; } 1250 } 1251 1252 _shouldRefresh(item, now) { 1253 return !item || now >= (item.exp - TOKEN_TTL_REFRESH_MS); 1254 } 1255 1256 /* 1257 * Mint a token for `host`, bounded by TOKEN_MINT_TIMEOUT_MS. 1258 * 1259 * THE TIMEOUT IS LOAD-BEARING, not defensive tidying. Without it this was 1260 * an unbounded fetch, and the failure it produced was worse than a slow 1261 * search: 1262 * 1263 * 1. A host that accepts the connection but never answers leaves this 1264 * promise pending forever. Callers await the token BEFORE any request 1265 * timeout of their own exists, so no caller budget can rescue it - the 1266 * hang happens strictly earlier than anything that could time it out. 1267 * 2. `inflight` then makes it contagious. Caching the pending promise is 1268 * correct and dedupes concurrent mints, but `finally` only clears the 1269 * entry once the promise SETTLES. One that never settles is never 1270 * cleared, so every later search for that host joins the same dead 1271 * promise. Reproduced by Vlad on #34: three searches in a row hung and 1272 * the database was never reached once. 1273 * 1274 * So one fix closes both. Aborting makes the promise reject, `finally` 1275 * runs, the entry clears, and the next search mints fresh. `inflight` 1276 * itself needed no change - it was only ever pathological because nothing 1277 * guaranteed settlement. 1278 * 1279 * The timer spans the body read as well as the headers. Clearing it once 1280 * the response object arrives would leave a stalled body unbounded, which 1281 * is the same bug one step further in. 1282 */ 1283 async _mint(host) { 1284 if (this.inflight.has(host)) return this.inflight.get(host); 1285 1286 const p = (async () => { 1287 const base = host.replace(/\/$/, ''); 1288 const url = `${base}/search-token`; 1289 1290 const controller = new AbortController(); 1291 const timer = setTimeout(() => controller.abort(), TOKEN_MINT_TIMEOUT_MS); 1292 try { 1293 const resp = await fetch(url, { 1294 credentials: IS_LOCAL ? 'omit' : 'include', 1295 signal: controller.signal, 1296 }); 1297 if (!resp.ok) throw new Error(`token_mint_failed:${resp.status}`); 1298 const { token, exp, sid } = await resp.json(); 1299 1300 const pack = { token, exp, sid, issuer: host }; 1301 this.cache.set(host, pack); 1302 try { sessionStorage.setItem(TOKEN_STORE_KEY_PREFIX + host, JSON.stringify(pack)); } catch {} 1303 if (this.bc) { this.bc.postMessage({ host, token, exp, sid, issuer: host }); } 1304 return pack; 1305 } finally { 1306 clearTimeout(timer); 1307 } 1308 })(); 1309 1310 this.inflight.set(host, p); 1311 try { return await p; } 1312 finally { this.inflight.delete(host); } 1313 } 1314 1315 /* 1316 * Drop a token the server has rejected. 1317 * 1318 * Freshness is decided by `_shouldRefresh` against the browser's own 1319 * Date.now(), so a client whose clock runs behind believes a long-dead 1320 * token is still valid and never re-mints. On 1 Sep 2026 one client 1321 * replayed the same expired token across every token-gated route for a 1322 * whole day without a single /search-token call, and had no way out: a 1323 * 401 left the cache untouched, so the next request replayed it again. 1324 * 1325 * Callers invoke this on a 401 to break that loop. The next get() misses 1326 * both caches and mints fresh, which is valid regardless of the clock. 1327 */ 1328 invalidate(host) { 1329 this.cache.delete(host); 1330 try { sessionStorage.removeItem(TOKEN_STORE_KEY_PREFIX + host); } catch {} 1331 } 1332 1333 async get(host, forcePossible = true) { 1334 const now = Date.now(); 1335 1336 let item = this.cache.get(host); 1337 if (!item) { 1338 const saved = this._loadFromSession(host); 1339 if (saved) { 1340 this.cache.set(host, saved); 1341 item = saved; 1342 } 1343 } 1344 1345 if (!item || this._shouldRefresh(item, now)) { 1346 return this._mint(host); 1347 } 1348 1349 if (forcePossible && (item.exp - now) < TOKEN_TTL_REFRESH_MS) { 1350 this._mint(host).catch(()=>{}); 1351 } 1352 1353 return item; 1354 } 1355} 1356 1357const tokenManager = new TokenManager(); 1358 1359async function getSearchTokenFor(base) { 1360 return tokenManager.get(base); 1361} 1362 1363/* 1364 * Exposed for callers that get a 401 from a token-gated route: purge, then 1365 * re-mint. See TokenManager.invalidate. 1366 */
1367function invalidateSearchTokenFor(base) { 1368 tokenManager.invalidate(base); 1369} 1370 1371// Playlists 1372let OriPicNum = 0; 1373let CurDocument = 0; 1374let CurPicNum = 0; 1375let CurUrl = ""; 1376 1377function preLoadPicture(pic) { 1378 heavyImage = new Image(); 1379 heavyImage.src = CurUrl + pic + ".jpg"; 1380} 1381 1382function getNextThumbNum(CurNum) { 1383 thumbs = new Array(1, 2, 3, 4, 5, 6); 1384 for (i = 0; i < thumbs.length; i++) { 1385 if (thumbs[i] > CurNum) { 1386 return thumbs[i]; 1387 } 1388 } 1389 return thumbs[0]; 1390} 1391 1392function nextThumbnail(idDoc) { 1393 if (CurDocument != 0 && idDoc == CurDocument) { 1394 document.getElementById("pic_" + CurDocument).src = CurUrl + CurPicNum + ".jpg"; 1395 nextthumb = getNextThumbNum(CurPicNum); 1396 preLoadPicture(nextthumb); 1397 CurPicNum = nextthumb; 1398 setTimeout("nextThumbnail(" + CurDocument + ")", 1000); 1399 } 1400} 1401 1402function stopThumbSlide() { 1403 TmpDoc = CurDocument; 1404 CurDocument = 0; 1405 document.getElementById("pic_" + TmpDoc).src = CurUrl + OriPicNum + ".jpg"; 1406} 1407 1408function startThumbSlide(IdDoc, CurNum, Url) { 1409 if (CurDocument == 0) { 1410 OriPicNum = CurNum; 1411 CurDocument = IdDoc; 1412 CurUrl = Url; 1413 nextthumb = getNextThumbNum(CurNum); 1414 preLoadPicture(nextthumb); 1415 CurPicNum = nextthumb; 1416 setTimeout("nextThumbnail(" + CurDocument + ")", 20); 1417 } 1418} 1419 1420 1421// Single movie and maybe for other pages as well 1422$('.encolsed-magnific-popup-wrap-up').each(function() { 1423 var $container = $(this); 1424 var $website_url = ''; 1425 var $imageLinks = $container.find('.media-list-item > a'); 1426 var items = []; 1427 $imageLinks.each(function() { 1428 var $item = $(this); 1429 var type = 'image'; 1430 title = $item.children('a').attr('lightbox'); 1431 if ($item.children('a').has('a[data-type="video"]')) { 1432 type = 'iframe'; 1433 1434 $vid_src = $item.children('div').children('a').attr('eid'); 1435 src = $website_url + $vid_src; 1436 } else { 1437 src = $item.children('a').attr('href'); 1438 } 1439 var magItem = { 1440 src: src, 1441 type: type, 1442 title: title 1443 }; 1444 if (magItem.src != '' && magItem.src.toLowerCase().indexOf(`view/${varValues.celeb_names.toLowerCase().slice(0, -1)}`) < 0 && magItem.src.toLowerCase().indexOf("/browse/") < 0) { 1445 items.push(magItem); 1446 } 1447 }); 1448 1449 $imageLinks.magnificPopup({ 1450 mainClass: 'mfp-fade', 1451 items: items, 1452 gallery: { 1453 enabled: true, 1454 tPrev: $(this).data('prev-text'), 1455 tNext: $(this).data('next-text') 1456 }, 1457 type: 'image', 1458 callbacks: { 1459 beforeOpen: function() { 1460 var index = $imageLinks.index(this.st.el); 1461 if (-1 !== index) { 1462 this.goTo(index); 1463 } 1464 } 1465 } 1466 }); 1467}); 1468 1469// Browse images and maybe for other pages as well 1470$('.enclosed-magnific-popup-wrap-up2').each(function() { 1471 var $container = $(this); 1472 var $imageLinks = $container.find('.media-list-item:not(.exo-item) a[lightbox]'); // even though it's story-image this actually targets all 1473 var items = []; 1474 $imageLinks.each(function() { 1475 var $item = $(this); 1476 1477 var type = 'image'; 1478 title = $item.attr('lightbox'); 1479 src = $item.attr('href'); 1480 var magItem = { 1481 src: src, 1482 type: type, 1483 title: title 1484 }; 1485 items.push(magItem); 1486 }); 1487 $imageLinks.magnificPopup({ 1488 mainClass: 'mfp-fade', 1489 items: items, 1490 gallery: { 1491 enabled: true, 1492 tPrev: $(this).data('prev-text'), 1493 tNext: $(this).data('next-text') 1494 }, 1495 type: 'image', 1496 callbacks: { 1497 beforeOpen: function() { 1498 var index = $imageLinks.index(this.st.el); 1499 if (-1 !== index) { 1500 this.goTo(index); 1501 } 1502 } 1503 } 1504 }); 1505}); 1506 1507function userHasAdBlockerOn() { 1508 return new Promise((resolve, reject) => {
1509 fetch('https://googleads.g.doubleclick.net/pagead/id', { 1510 method: "HEAD", 1511 mode: "no-cors", 1512 cache: "no-store", 1513 }) 1514 .then(() => resolve(false)) 1515 .catch(() => resolve(true)); 1516 }) 1517} 1518 1519// New Navbar 1520document.addEventListener("DOMContentLoaded", function() { 1521 // **Hamburger Menu (Vanilla JS)** 1522 const hamburgerDropdown = document.querySelector('.hamburger-dropdown'); 1523 const hamburgerOverlay = document.querySelector('.hamburger-dropdown-overlay'); 1524 const hamburgerToggler = document.getElementById('hamburger-menu'); 1525 1526 function closeHamburgerMenu() { 1527 hamburgerToggler.setAttribute('data-nav-status', 'closed'); 1528 hamburgerDropdown.classList.remove('hamburger-dropdown--show'); 1529 hamburgerOverlay.classList.remove('hamburger-dropdown-overlay--show'); 1530 setTimeout(() => { 1531 hamburgerDropdown.style.display = 'none'; 1532 hamburgerOverlay.style.display = 'none'; 1533 document.body.style.overflow = 'auto'; 1534 }, window.screen.width < 1024 ? 100 : 0); 1535 } 1536 1537 function openHamburgerMenu() { 1538 hamburgerToggler.setAttribute('data-nav-status', 'open'); 1539 hamburgerDropdown.style.display = 'block'; 1540 hamburgerOverlay.style.display = 'block'; 1541 setTimeout(() => { 1542 hamburgerDropdown.classList.add('hamburger-dropdown--show'); 1543 hamburgerOverlay.classList.add('hamburger-dropdown-overlay--show'); 1544 if (window.innerWidth < 1024) document.body.style.overflow = 'hidden'; 1545 }, 50); 1546 hamburgerDropdown.querySelectorAll('.hamburger-dropdown__menu-item')[1]?.focus(); 1547 } 1548 1549 hamburgerToggler.addEventListener('click', () => { 1550 hamburgerToggler.getAttribute('data-nav-status') === 'closed' ? openHamburgerMenu() : closeHamburgerMenu(); 1551 }); 1552 1553 document.querySelector('.hamburger-dropdown__close')?.addEventListener('click', (event) => { 1554 event.stopPropagation(); 1555 closeHamburgerMenu(); 1556 }); 1557 1558 document.addEventListener('keydown', (event) => { 1559 if (event.target === hamburgerToggler && event.key === 'Enter') { 1560 hamburgerToggler.click(); 1561 } 1562 }); 1563 1564 document.addEventListener('click', (event) => { 1565 if (!event.target.closest('#hamburger-menu') && 1566 !event.target.closest('.hamburger-dropdown') && 1567 hamburgerDropdown.classList.contains('hamburger-dropdown--show')) { 1568 closeHamburgerMenu(); 1569 } 1570 }); 1571 1572 hamburgerOverlay.addEventListener('click', closeHamburgerMenu); 1573 1574 let resizeTimeout; 1575 window.addEventListener('resize', () => { 1576 clearTimeout(resizeTimeout); 1577 resizeTimeout = setTimeout(() => { 1578 if (window.innerWidth > 1024 && hamburgerDropdown.classList.contains('hamburger-dropdown--show')) { 1579 closeHamburgerMenu(); 1580 } 1581 }, 100); 1582 }); 1583 1584 // **Mega Menus Positioning** 1585 const subNav = document.querySelector('.sub-nav'); 1586 function positionMegaMenu(megaMenu) { 1587 const subNavRect = subNav.getBoundingClientRect(); 1588 megaMenu.style.width = '100%'; 1589 megaMenu.style.left = `-${subNavRect.left}px`; 1590 megaMenu.style.top = `${subNavRect.bottom}px`; 1591 } 1592 1593 // **Generalized Mega Menus** 1594 const megaMenuParents = document.querySelectorAll('[data-mega-menu]'); 1595 megaMenuParents.forEach(parent => { 1596 const targetName = parent.getAttribute('data-mega-menu'); 1597 const megaMenu = document.querySelector(`[data-mega-menu-target="${targetName}"]`); 1598 let showTimer, hideTimer, isOverParent = false, isOverMenu = false;
1599 1600 if (megaMenu) { 1601 parent.addEventListener('mouseenter', () => { 1602 clearTimeout(hideTimer); 1603 isOverParent = true; 1604 parent.classList.add('hover'); 1605 showTimer = setTimeout(() => { 1606 megaMenu.style.display = 'block'; 1607 positionMegaMenu(megaMenu); 1608 megaMenu.style.opacity = '1'; 1609 }, 250); 1610 }); 1611 1612 parent.addEventListener('mouseleave', () => { 1613 clearTimeout(showTimer); 1614 isOverParent = false; 1615 parent.classList.remove('hover'); 1616 hideTimer = setTimeout(() => { 1617 if (!isOverMenu) { 1618 megaMenu.style.opacity = '0'; 1619 setTimeout(() => megaMenu.style.display = 'none', 250); 1620 } 1621 }, 250); 1622 }); 1623 1624 megaMenu.addEventListener('mouseenter', () => { 1625 clearTimeout(hideTimer); 1626 isOverMenu = true; 1627 parent.classList.add('hover'); 1628 }); 1629 1630 megaMenu.addEventListener('mouseleave', () => { 1631 clearTimeout(hideTimer); 1632 isOverMenu = false;
1633 parent.classList.remove('hover'); 1634 hideTimer = setTimeout(() => { 1635 if (!isOverParent) { 1636 megaMenu.style.opacity = '0'; 1637 setTimeout(() => megaMenu.style.display = 'none', 250); 1638 } 1639 }, 250); 1640 }); 1641 } 1642 }); 1643 1644 window.addEventListener('resize', () => { 1645 document.querySelectorAll('[data-mega-menu-target]').forEach(megaMenu => { 1646 if (megaMenu.style.display === 'block') positionMegaMenu(megaMenu); 1647 }); 1648 }); 1649 1650 // **Categories Rendering** 1651 const megaMenuContainers = document.querySelectorAll('.mega-menu-categories'); 1652 if(varValues.navbarCategories && megaMenuContainers){ 1653 const fansCategories = { 1654 "Straight": [ 1655 {category:"Explicit", video_count:4031}, 1656 {category:"Masturbation", video_count:1503}, 1657 {category:"Blowjob", video_count:793}, 1658 {category:"Lesbian", video_count:204}, 1659 {category:"Anal", video_count:349}, 1660 {category:"Topless", video_count:1234}, 1661 {category:"Thong", video_count:850}, 1662 {category:"Bikini", video_count:869}, 1663 {category:"Underwear", video_count:78}, 1664 {category:"Cumshot", video_count:251}, 1665 {category:"Bush", video_count:49}, 1666 {category:"Fetish", video_count:67} 1667 ], 1668 "Gay": [ 1669 {category:"Penis", video_count:366}, 1670 {category:"Shirtless", video_count:131}, 1671 {category:"Anal", video_count:25}, 1672 {category:"Masturbation", video_count:1}, 1673 {category:"Blowjob", video_count:14}, 1674 {category:"Cumshot", video_count:19}, 1675 {category:"Bulge", video_count:35}, 1676 {category:"Threesome", video_count:1}, 1677 {category:"Butt", video_count:54}, 1678 {category:"Muscular", video_count:1}, 1679 {category:"Handjob", video_count:8}, 1680 {category:"Bareback", video_count:5} 1681 ], 1682 "Trans": [ 1683 {category:"Explicit", video_count:2}, 1684 {category:"Penis", video_count:209}, 1685 {category:"Anal", video_count:6}, 1686 {category:"Blowjob", video_count:3}, 1687 {category:"Boobs", video_count:3}, 1688 {category:"Masturbation", video_count:3}, 1689 {category:"Cumshot", video_count:2}, 1690 {category:"Ladyboy", video_count:2}, 1691 {category:"Butt", video_count:15}, 1692 {category:"Dildo", video_count:10}, 1693 {category:"Handjob", video_count:4}, 1694 {category:"Lingerie", video_count:14} 1695 ] 1696 } 1697 1698 if(localStorage.getItem('sexuality')) varValues.navbarCategories = fansCategories[localStorage.getItem('sexuality')]; 1699 1700 megaMenuContainers.forEach(container => { 1701 const html = varValues.navbarCategories.map(category => ` 1702 <div class="mega-menu-categories-item"> 1703 <a href="${localStorage.getItem('sexuality')?.toLowerCase() || ''}/tags/vids/${category.category.toLowerCase()}/1.html" aria-label="View ${category.category} category with ${category.video_count} videos"> 1704 <img class="mega-menu-categories-item-img" src="${varValues.navbarCategoriesImageUrlPrefix}${localStorage.getItem('sexuality')?.toLowerCase() || ''}/${category.category.toLowerCase()}.jpg" alt="${category.category}" loading="lazy"> 1705 <div class="text-overlay"> 1706 <span class="category-name">${category.category}</span> 1707 <span class="video-count">${category.video_count} videos</span> 1708 </div> 1709 </a> 1710 </div> 1711 `).join(''); 1712 1713 container.innerHTML = html; 1714 }); 1715 } 1716 1717 const categoriesToggle = document.querySelector('.categories-toggle'); 1718 const categoriesDropdown = document.querySelector('.categories-dropdown-content'); 1719 const dropdownArrow = categoriesToggle.querySelector('.dropdown-arrow'); 1720 const categoriesContent = document.createElement('div'); 1721 categoriesContent.className = 'mega-menu-categories'; 1722 categoriesDropdown.appendChild(categoriesContent); 1723 categoriesDropdown.classList.add('open'); 1724 dropdownArrow.classList.add('rotated'); 1725 categoriesToggle.addEventListener('click', e => { 1726 e.preventDefault(); 1727 const isOpen = categoriesDropdown.classList.contains('open'); 1728 dropdownArrow.classList.toggle('rotated', !isOpen); 1729 categoriesDropdown.classList.toggle('open'); 1730 }); 1731 1732 // **Scrollbars** 1733 const navMenu = document.querySelector('.sub-nav-menu'); 1734 if (navMenu) { 1735 navMenu.style.overflowX = 'auto'; 1736 const scrollbar = document.createElement('div'); 1737 scrollbar.className = 'custom-scrollbar'; 1738 scrollbar.innerHTML = '<div class="scrollbar-track"><div class="scrollbar-thumb"></div></div>'; 1739 navMenu.parentNode.insertBefore(scrollbar, navMenu.nextSibling); 1740 const thumb = scrollbar.querySelector('.scrollbar-thumb'); 1741 1742 function updateThumb() { 1743 const contentWidth = navMenu.scrollWidth, containerWidth = navMenu.clientWidth; 1744 if (contentWidth <= containerWidth) return scrollbar.style.display = 'none'; 1745 scrollbar.style.display = 'block'; 1746 const ratio = containerWidth / contentWidth; 1747 thumb.style.width = `${ratio * containerWidth}px`; 1748 const scrollLeft = navMenu.scrollLeft, maxScroll = contentWidth - containerWidth; 1749 thumb.style.left = `${maxScroll ? (scrollLeft / maxScroll) * (containerWidth - thumb.clientWidth) : 0}px`; 1750 } 1751 updateThumb(); 1752 1753 navMenu.addEventListener('scroll', updateThumb, { passive: true }); 1754 window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(updateThumb, 100); }); 1755 1756 let isDragging = false, dragStartX, thumbStartLeft; 1757 thumb.addEventListener('mousedown', e => { 1758 isDragging = true; 1759 dragStartX = e.clientX; 1760 thumbStartLeft = parseInt(thumb.style.left || '0', 10); 1761 document.body.style.userSelect = 'none'; 1762 }); 1763 document.addEventListener('mousemove', e => { 1764 if (!isDragging) return; 1765 const dx = e.clientX - dragStartX, newThumbLeft = thumbStartLeft + dx; 1766 const maxThumbLeft = navMenu.clientWidth - thumb.clientWidth; 1767 thumb.style.left = `${Math.max(0, Math.min(newThumbLeft, maxThumbLeft))}px`; 1768 navMenu.scrollLeft = (parseInt(thumb.style.left) / maxThumbLeft) * (navMenu.scrollWidth - navMenu.clientWidth); 1769 }); 1770 document.addEventListener('mouseup', () => { 1771 if (isDragging) { isDragging = false; document.body.style.userSelect = ''; } 1772 }); 1773 } 1774 1775 const hamburgerMenu = document.querySelector('.hamburger-dropdown__menu'); 1776 const hamburgerMenuContainer = document.querySelector('.hamburger-dropdown__menu-container'); 1777 if (hamburgerMenu && hamburgerMenuContainer) { 1778 const scrollbarVertical = document.createElement('div'); 1779 scrollbarVertical.className = 'custom-scrollbar-vertical'; 1780 scrollbarVertical.innerHTML = '<div class="scrollbar-track-vertical"><div class="scrollbar-thumb-vertical"></div></div>'; 1781 hamburgerMenuContainer.appendChild(scrollbarVertical); 1782 const thumbVertical = scrollbarVertical.querySelector('.scrollbar-thumb-vertical'); 1783 1784 function updateThumbVertical() { 1785 const contentHeight = hamburgerMenu.scrollHeight, containerHeight = hamburgerMenu.clientHeight; 1786 if (contentHeight <= containerHeight) return scrollbarVertical.style.display = 'none'; 1787 scrollbarVertical.style.display = 'block'; 1788 const ratio = containerHeight / contentHeight; 1789 thumbVertical.style.height = `${ratio * containerHeight}px`; 1790 const scrollTop = hamburgerMenu.scrollTop, maxScroll = contentHeight - containerHeight; 1791 thumbVertical.style.top = `${maxScroll ? (scrollTop / maxScroll) * (containerHeight - thumbVertical.clientHeight) : 0}px`; 1792 } 1793 updateThumbVertical(); 1794 1795 hamburgerMenu.addEventListener('scroll', updateThumbVertical, { passive: true }); 1796 window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(updateThumbVertical, 100); }); 1797 new MutationObserver(updateThumbVertical).observe(hamburgerMenu, { childList: true, subtree: true, attributes: true }); 1798 1799 let isDraggingVertical = false, dragStartY, thumbStartTop; 1800 thumbVertical.addEventListener('mousedown', e => { 1801 isDraggingVertical = true; dragStartY = e.clientY; thumbStartTop = parseInt(thumbVertical.style.top || '0', 10); 1802 document.body.style.userSelect = 'none'; 1803 }); 1804 document.addEventListener('mousemove', e => { 1805 if (!isDraggingVertical) return; 1806 const dy = e.clientY - dragStartY, newThumbTop = thumbStartTop + dy; 1807 const maxThumbTop = hamburgerMenu.clientHeight - thumbVertical.clientHeight; 1808 thumbVertical.style.top = `${Math.max(0, Math.min(newThumbTop, maxThumbTop))}px`; 1809 hamburgerMenu.scrollTop = (parseInt(thumbVertical.style.top) / maxThumbTop) * (hamburgerMenu.scrollHeight - hamburgerMenu.clientHeight); 1810 }); 1811 document.addEventListener('mouseup', () => { 1812 if (isDraggingVertical) { isDraggingVertical = false; document.body.style.userSelect = ''; } 1813 }); 1814 } 1815 1816 const azToggleMobile = document.querySelector('.hamburger-a-z-toggle'); 1817 const azDropdownMobile = document.querySelector('.hamburger-a-z-dropdown-content'); 1818 azToggleMobile.addEventListener('click', e => { 1819 e.preventDefault(); 1820 const isOpen = azDropdownMobile.classList.contains('open'); 1821 azDropdownMobile.classList.toggle('open'); 1822 azToggleMobile.querySelector('.dropdown-arrow')?.classList.toggle('rotated', !isOpen); 1823 }); 1824 1825 // **Search Functionality** 1826 const searchToggle = document.getElementById("search-mobile-toggle"); 1827 const searchContainer = document.querySelector(".search-mobile-container"); 1828 const searchInput = document.getElementById("search-mobile-input"); 1829 const searchCloseBtn = document.getElementById("search-close-btn"); 1830 const trendingContainer = document.querySelector(".trending-searches-container"); 1831 const trendingList = document.querySelector(".trending-searches-list"); 1832 const suggestionsContainer = document.querySelector(".suggestions-container"); 1833 const searchInputDesktop = document.querySelector(".search-bar input[name='q']"); 1834 const searchFormDesktop = document.querySelector(".search-bar form"); 1835 1836 let dropdownDesktop;
1837 let cachedTrendingSearches = null; 1838 let cachedTrendingCelebs = null; 1839 1840 let activeCategoryDesktop = 'all'; 1841 let activeCategoryMobile = 'all'; 1842 let lastResultsDesktop = null; 1843 let lastResultsMobile = null; 1844 1845 const suggestionsCache = Object.create(null); 1846 let lastQueryDesktop = ''; 1847 let lastQueryMobile = ''; 1848 1849 function adaptiveDebounce(fn, opts = {}) { 1850 const { 1851 min = 100, 1852 max = 600, 1853 k = 1.4, 1854 alpha = 0.4, 1855 idleReset = 900, 1856 shortFloor = 260, 1857 leading = false, 1858 fastThreshold = 140, 1859 fastFloor = 360 1860 } = opts; 1861 1862 let ema = null, lastTs = 0, tId = null, lastValue = "", lastFired = ""; 1863 let controller = null, idle = true; 1864 1865 const lastDeltas = []; 1866 1867 const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); 1868 const now = () => (performance?.now?.() ?? Date.now()); 1869 1870 function computeDelay(value, delta) { 1871 if (delta != null) { 1872 ema = ema == null ? delta : (alpha * delta + (1 - alpha) * ema); 1873 lastDeltas.push(delta); 1874 if (lastDeltas.length > 3) lastDeltas.shift(); 1875 } 1876 1877 let base = ema == null ? shortFloor : k * ema; 1878 1879 if (value.length <= 2) base = Math.max(base, shortFloor); 1880 1881 const isFastBurst = lastDeltas.length >= 2 && lastDeltas.every(d => d < fastThreshold); 1882 if (isFastBurst) base = Math.max(base, fastFloor); 1883 1884 if (/[ \t.,!?;:)]$/.test(value)) base = Math.min(base, min); 1885 1886 return clamp(Math.round(base), min, max); 1887 } 1888 1889 async function fire(value) { 1890 tId = null; 1891 if (value === lastFired) return; 1892 lastFired = value; 1893 1894 if (controller) controller.abort(); 1895 controller = new AbortController(); 1896 1897 try { 1898 await fn(value, { signal: controller.signal }); 1899 } catch (err) {} 1900 } 1901 1902 function schedule(value) { 1903 const ts = now(); 1904 const delta = lastTs ? (ts - lastTs) : null; 1905 const isIdleBurst = lastTs && (ts - lastTs) >= idleReset; 1906 lastTs = ts; 1907 1908 if ((idle || isIdleBurst) && leading && value.length > 0) { 1909 idle = false; 1910 fire(value); 1911 return; 1912 } 1913 1914 idle = false; 1915 const delay = computeDelay(value, delta); 1916 if (tId) clearTimeout(tId); 1917 tId = setTimeout(() => fire(value), delay); 1918 } 1919 1920 function wrapper(value) { 1921 if (!value) { 1922 if (tId) clearTimeout(tId); 1923 tId = null; 1924 lastFired = ""; 1925 if (controller) controller.abort(); 1926 controller = null; 1927 idle = true; 1928 lastDeltas.length = 0; 1929 return; 1930 } 1931 if (lastTs && (now() - lastTs) >= idleReset) { 1932 idle = true; 1933 lastDeltas.length = 0; 1934 } 1935 lastValue = value; 1936 schedule(value); 1937 } 1938 1939 wrapper.flush = () => { 1940 if (tId) { 1941 clearTimeout(tId); 1942 tId = null; 1943 fire(lastValue); 1944 } 1945 }; 1946 1947 wrapper.cancel = () => { 1948 if (tId) clearTimeout(tId); 1949 tId = null; 1950 if (controller) controller.abort(); 1951 controller = null; 1952 }; 1953 1954 return wrapper; 1955 } 1956 1957 function escapeRegExp(str) { 1958 return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 1959 } 1960 1961 async function fetchWithTimeout(url, options, timeout = 1500) { 1962 const controller = new AbortController(); 1963 const timeoutId = setTimeout(() => controller.abort(), timeout); 1964 try { 1965 const response = await fetch(url, { ...options, signal: controller.signal }); 1966 clearTimeout(timeoutId); 1967 return response; 1968 } catch (error) { 1969 clearTimeout(timeoutId); 1970 throw error; 1971 } 1972 } 1973 1974 async function safeFetch(primaryUrl, fallbackUrl, options = {}, timeout = 1500) { 1975 try { 1976 const response = await fetchWithTimeout(primaryUrl, options, timeout); 1977 if (!response.ok) throw new Error(`Primary fetch failed: ${response.status}`); 1978 return response; 1979 } catch (error) { 1980 if (error.name === 'AbortError') { 1981 console.warn(`Primary URL timed out after ${timeout}ms`); 1982 } 1983 try { 1984 const response = await fetchWithTimeout(fallbackUrl, options, timeout); 1985 if (!response.ok) throw new Error(`Fallback fetch failed: ${response.status}`); 1986 return response; 1987 } catch (fallbackError) { 1988 if (fallbackError.name === 'AbortError') { 1989 throw new Error(`Fallback URL timed out after ${timeout}ms`); 1990 } 1991 throw fallbackError; 1992 } 1993 } 1994 } 1995 1996 function renderCelebItem(item, q = '') { 1997 let { thumb, url, text } = item; 1998 thumb = varValues.urlPrefixCelebsMovies + thumb.replace('/portfilio/', '/circularpic/'); 1999 url = varValues.domainFull + url; 2000 2001 const nameHighlighted = q 2002 ? text.replace(new RegExp(escapeRegExp(q), 'gi'), m => `<b>${m}</b>`) 2003 : text; 2004 2005 return ` 2006 <div class="trending-celeb-item"> 2007 <a href="${url}"> 2008 <div class="trending-celeb-content"> 2009 <div class="trending-celeb-thumb"> 2010 <img src="${thumb}" alt="${nameHighlighted} profile image" loading="lazy"> 2011 </div> 2012 <div class="trending-celeb-info"> 2013 <div class="trending-celeb-name">${nameHighlighted}</div> 2014 </div> 2015 </div> 2016 </a> 2017 </div> 2018 `; 2019 } 2020 2021 function renderTrendingCelebsSection(data, q = '') { 2022 return ` 2023 <div class="trending-celebs-list"> 2024 ${data.map(d => renderCelebItem(d, q)).join('')} 2025 </div> 2026 `; 2027 } 2028 2029 function getCacheKey(q) { 2030 return String(q || '').trim().toLowerCase(); 2031 } 2032 2033 function getCacheForQuery(q) {
2034 const key = getCacheKey(q); 2035 if (!suggestionsCache[key]) { 2036 suggestionsCache[key] = { initial: null, fullByType: {} }; 2037 } 2038 return suggestionsCache[key]; 2039 } 2040 2041 function getRecentSearches(limit = 8) { 2042 try { 2043 const raw = localStorage.getItem("recentSearches"); 2044 if (!raw) return []; 2045 const arr = JSON.parse(raw); 2046 if (!Array.isArray(arr)) return []; 2047 const deduped = []; 2048 for (let i = 0; i < arr.length && deduped.length < limit; i++) { 2049 const term = String(arr[i]).trim(); 2050 if (term && !deduped.includes(term)) deduped.push(term); 2051 } 2052 return deduped.slice(0, limit); 2053 } catch { 2054 return []; 2055 } 2056 } 2057 2058 function setRecentSearches(arr) { 2059 if (arr.length === 0) { 2060 localStorage.removeItem("recentSearches"); 2061 } else { 2062 localStorage.setItem("recentSearches", JSON.stringify(arr)); 2063 } 2064 } 2065 2066 function removeRecentItem(term) { 2067 const current = getRecentSearches(9999); 2068 const next = current.filter(t => t !== term); 2069 setRecentSearches(next); 2070 2071 document.querySelectorAll('li[data-term="' + term + '"]').forEach(li => li.remove()); 2072 2073 const anyLeft = getRecentSearches(1).length > 0; 2074 const desktopGroup = dropdownDesktop ? dropdownDesktop.querySelector('[data-group="recent"]') : null; 2075 const mobileContainer = document.querySelector(".recent-searches-container"); 2076 2077 if (!anyLeft) { 2078 if (desktopGroup) desktopGroup.remove(); 2079 if (mobileContainer) mobileContainer.remove(); 2080 } 2081 } 2082 2083 function clearAllRecent() { 2084 localStorage.removeItem("recentSearches"); 2085 2086 const desktopGroup = dropdownDesktop ? dropdownDesktop.querySelector('[data-group="recent"]') : null; 2087 if (desktopGroup) desktopGroup.remove(); 2088 2089 const mobileContainer = document.querySelector(".recent-searches-container"); 2090 if (mobileContainer) mobileContainer.remove(); 2091 } 2092 2093 async function fetchTrendingCelebs() { 2094 if (cachedTrendingCelebs) return cachedTrendingCelebs; 2095 try { 2096 const resp = await fetch(`https://${varValues.cdn}/js/search/trending-celebs.json`); 2097 if (!resp.ok) throw new Error("Failed to load trending celebs"); 2098 const items = await resp.json(); 2099 cachedTrendingCelebs = items; 2100 return cachedTrendingCelebs; 2101 } catch { 2102 cachedTrendingCelebs = []; 2103 return []; 2104 } 2105 } 2106 2107 async function fetchTrendingSearches() { 2108 if (cachedTrendingSearches) return cachedTrendingSearches; 2109 try { 2110 let countryCode = 'US'; 2111 const countryCookie = getCookie("countryData"); 2112 if (countryCookie) { 2113 try { 2114 const data = JSON.parse(countryCookie); 2115 if (data.country_code) countryCode = data.country_code; 2116 } catch (e) { 2117 console.error('Error parsing country cookie:', e); 2118 } 2119 } 2120 2121 let sexualityParam = ''; 2122 if (varValues.gender === 'fans' || varValues.gender === 'feet') { 2123 const s = localStorage.getItem('sexuality'); 2124 const mapped = s === 'Straight' ? 'f' : s === 'Gay' ? 'm' : s === 'Trans' ? 't' : 'f'; 2125 sexualityParam = `&sexuality=${mapped}`; 2126 } 2127 2128 const { token, sid } = await getSearchTokenFor(PRIMARY_BASE); // minted on the origin, always 2129 const data = await withEdgeFallback((base) => 2130 fetchTrendingFrom(base, { token, sid, gender: varValues.gender, countryCode, sexualityParam }) 2131 ); 2132 const items = Array.isArray(data?.data) ? data.data : []; 2133 cachedTrendingSearches = items.map(i => i.query).filter(Boolean).slice(0, 12); 2134 return cachedTrendingSearches; 2135 } catch (err) { 2136 console.error('Error fetching trending searches:', err); 2137 cachedTrendingSearches = []; 2138 return []; 2139 } 2140 } 2141 2142 searchFormDesktop.addEventListener("click", async (e) => { 2143 if (e.target.closest(".search-suggestions-dropdown")) return; 2144 2145 searchInputDesktop.focus(); 2146 2147 const val = searchInputDesktop.value.trim();
2148 const key = getCacheKey(val); 2149 2150 if (val.length >= 2) { 2151 if (dropdownDesktop && dropdownDesktop.style.display !== "none" && lastResultsDesktop && lastQueryDesktop === key) return; 2152 await renderDesktopByQuery(val); 2153 } else { 2154 if (dropdownDesktop && dropdownDesktop.style.display !== "none" && !lastResultsDesktop) return; 2155 await renderExploreDesktop(); 2156 } 2157 }); 2158 2159 function ensureDesktopDropdown() { 2160 if (dropdownDesktop) return dropdownDesktop; 2161 dropdownDesktop = document.createElement("div"); 2162 dropdownDesktop.className = "search-suggestions-dropdown"; 2163 searchFormDesktop.appendChild(dropdownDesktop); 2164 2165 dropdownDesktop.addEventListener("click", function(e) { 2166 const clearBtn = e.target.closest(".clear-all-btn"); 2167 const removeBtn = e.target.closest(".remove-recent-btn"); 2168 2169 if (clearBtn || removeBtn) { 2170 e.preventDefault(); 2171 e.stopPropagation(); 2172 } 2173 2174 if (clearBtn) { 2175 clearAllRecent(); 2176 return; 2177 } 2178 if (removeBtn) { 2179 const term = removeBtn.getAttribute("data-term"); 2180 if (term) removeRecentItem(term); 2181 } 2182 }); 2183 2184 dropdownDesktop.addEventListener("mousedown", function(e) { 2185 if (e.target.closest(".clear-all-btn") || e.target.closest(".remove-recent-btn")) { 2186 e.stopPropagation(); 2187 } 2188 }); 2189 2190 return dropdownDesktop; 2191 } 2192 2193 function setDesktopExploreSkeleton() { 2194 const dd = ensureDesktopDropdown(); 2195 dd.innerHTML = ` 2196 <div class="explore-groups"> 2197 <div class="explore-group" data-group="trending-searches"> 2198 <h3 class="explore-title">Trending Searches</h3> 2199 <ul class="explore-list explore-trending"></ul> 2200 </div> 2201 <div class="explore-group" data-group="trending-celebs"> 2202 <div class="explore-title-row"> 2203 <h3 class="explore-title">Trending Celebs</h3> 2204 <a href="/browse/celebs/popular/days/1.html" class="view-all-btn">View All</a> 2205 </div> 2206 <div class="vertical-list-mount"></div> 2207 </div> 2208 </div> 2209 `; 2210 return dd; 2211 } 2212 2213 function buildRecentItem(term) { 2214 return ` 2215 <li class="recent-row" data-term="${term}"> 2216 <a href="/search.html?q=${encodeURIComponent(term)}">${term}</a> 2217 <button type="button" class="remove-recent-btn" aria-label="Remove" data-term="${term}">Ã</button> 2218 </li> 2219 `; 2220 } 2221 2222 function fillListSimple(ul, items) { 2223 if (!ul) return; 2224 ul.innerHTML = items.map(s => `<li><a href="/search.html?q=${encodeURIComponent(s)}">${s}</a></li>`).join(""); 2225 } 2226 2227 function fillRecentList(ul, items) { 2228 if (!ul) return; 2229 ul.innerHTML = items.map(s => buildRecentItem(s)).join(""); 2230 } 2231 2232 async function renderExploreDesktop() { 2233 const dd = setDesktopExploreSkeleton(); 2234 2235 const recent = getRecentSearches(); 2236 const trending = await fetchTrendingSearches(); 2237 2238 // Conditionally inject Recent Searches group only if there is at least one item 2239 if (recent.length > 0) { 2240 const recentGroup = document.createElement("div"); 2241 recentGroup.className = "explore-group"; 2242 recentGroup.setAttribute("data-group", "recent"); 2243 recentGroup.innerHTML = ` 2244 <div class="explore-title-row"> 2245 <h3 class="explore-title">Recent Searches</h3> 2246 <button type="button" class="clear-all-btn" data-target="recent">Clear all</button> 2247 </div> 2248 <ul class="explore-list explore-recent"></ul> 2249 `; 2250 const groupsRoot = dd.querySelector(".explore-groups"); 2251 const trendingBlock = dd.querySelector('[data-group="trending-searches"]'); 2252 if (groupsRoot) { 2253 if (trendingBlock) { 2254 groupsRoot.insertBefore(recentGroup, trendingBlock); 2255 } else {
2256 groupsRoot.prepend(recentGroup); 2257 } 2258 } 2259 } 2260 2261 const recentUL = dd.querySelector(".explore-recent"); 2262 const trendingUL = dd.querySelector(".explore-trending"); 2263 const celebsMount = dd.querySelector(".vertical-list-mount"); 2264 2265 if (trendingUL) { 2266 fillListSimple(trendingUL, trending); 2267 } 2268 if (recentUL) { 2269 fillRecentList(recentUL, recent); 2270 } 2271 if (celebsMount) { 2272 const celebs = await fetchTrendingCelebs(); 2273 celebsMount.innerHTML = renderTrendingCelebsSection(celebs); 2274 } 2275 2276 dd.style.display = "block"; 2277 } 2278 2279 function hideDesktopDropdown() { 2280 if (dropdownDesktop) dropdownDesktop.style.display = "none"; 2281 if (typeof remoteSearchDesktop?.cancel === "function") remoteSearchDesktop.cancel(); 2282 } 2283 2284 async function ensureMobileExploreContainers() { 2285 let recentC = document.querySelector(".recent-searches-container"); 2286 if (!recentC) { 2287 recentC = document.createElement("div"); 2288 recentC.className = "recent-searches-container"; 2289 recentC.innerHTML = ` 2290 <div class="mobile-title-row"> 2291 <h2 class="explore-title">Recent Searches</h2> 2292 <button type="button" class="clear-all-btn" data-target="recent">Clear all</button> 2293 </div> 2294 <ul class="recent-searches-list"></ul> 2295 `; 2296 const anchor = document.querySelector(".trending-searches-container"); 2297 if (anchor) anchor.insertAdjacentElement("beforebegin", recentC); 2298 else searchContainer.appendChild(recentC); 2299 2300 recentC.addEventListener("click", function(e) { 2301 const clearBtn = e.target.closest(".clear-all-btn"); 2302 const removeBtn = e.target.closest(".remove-recent-btn"); 2303 2304 if (clearBtn || removeBtn) { 2305 e.preventDefault(); 2306 e.stopPropagation(); 2307 } 2308 2309 if (clearBtn) { 2310 clearAllRecent(); 2311 return; 2312 } 2313 if (removeBtn) { 2314 const term = removeBtn.getAttribute("data-term"); 2315 if (term) removeRecentItem(term); 2316 } 2317 }); 2318 } 2319 2320 let celebsC = document.querySelector(".trending-celebs-container"); 2321 if (!celebsC) { 2322 celebsC = document.createElement("div"); 2323 celebsC.className = "trending-celebs-container"; 2324 celebsC.innerHTML = ` 2325 <div class="explore-title-row"> 2326 <h2 class="explore-title">Trending Celebs</h2> 2327 <a href="/browse/celebs/popular/days/1.html" class="view-all-btn">View All</a> 2328 </div> 2329 <div class="vertical-list-mount"></div> 2330 `; 2331 searchContainer.appendChild(celebsC); 2332 } 2333 2334 let mobileResults = document.querySelector(".suggestions-container"); 2335 if (mobileResults && !mobileResults.querySelector(".results-host")) { 2336 const host = document.createElement("div"); 2337 host.className = "results-host"; 2338 mobileResults.appendChild(host); 2339 } 2340 } 2341 2342 async function renderExploreMobile() { 2343 await ensureMobileExploreContainers(); 2344 2345 const trending = await fetchTrendingSearches(); 2346 const recent = getRecentSearches(); 2347 2348 if (trendingList) { 2349 trendingList.innerHTML = trending 2350 .map(s => `<li><a href="/search.html?q=${encodeURIComponent(s)}">${s}</a></li>`) 2351 .join(""); 2352 } 2353 2354 const recentUL = document.querySelector(".recent-searches-list"); 2355 if (recentUL) { 2356 fillRecentList(recentUL, recent); 2357 } 2358 2359 const celebsMount = document.querySelector(".trending-celebs-container .vertical-list-mount"); 2360 if (celebsMount) { 2361 const celebs = await fetchTrendingCelebs(); 2362 celebsMount.innerHTML = renderTrendingCelebsSection(celebs); 2363 } 2364 2365 const recentC = document.querySelector(".recent-searches-container"); 2366 const celebsC = document.querySelector(".trending-celebs-container"); 2367 2368 if (trendingContainer) { 2369 trendingContainer.style.display = "block"; 2370 } 2371 if (recentC) { 2372 recentC.style.display = recent && recent.length > 0 ? "block" : "none"; 2373 } 2374 if (celebsC) { 2375 celebsC.style.display = "block"; 2376 } 2377 if (suggestionsContainer) { 2378 suggestionsContainer.style.display = "none"; 2379 } 2380 } 2381 2382 function normalizeResultsPayload(payload) { 2383 const empty = { celebs: [], movies: [], stories: [], videos: [] }; 2384 const data = payload?.data || empty; 2385 const isFeet = varValues.gender === 'feet'; 2386 2387 const mapType = (arr, type) => (Array.isArray(arr) ? arr : []).map(item => { 2388 let { url, text, avg_rating, celeb_id } = item; 2389 // Feet celebs: /view/celeb/m/madonna.html -> /view/celeb/m/34620/madonna.html 2390 if (isFeet && type === 'celebs' && celeb_id && url && url.startsWith('/view/celeb/')) { 2391 const parts = url.split('/');
2392 parts.splice(4, 0, String(celeb_id)); 2393 url = parts.join('/'); 2394 } 2395 if (type === 'celebs' || type === 'movies' || type === 'stories' || type === 'videos') { 2396 url = varValues.domainFull + url; 2397 } 2398 return { url, text, avg_rating }; 2399 }); 2400 2401 return { 2402 celebs: mapType(data.celebs, 'celebs'), 2403 movies: mapType(data.movies, 'movies'), 2404 stories: mapType(data.stories, 'stories'), 2405 videos: mapType(data.videos, 'videos') 2406 }; 2407 } 2408 2409 function buildTabs(counts, active) { 2410 return ` 2411 <div class="media__picker"> 2412 <button type="button" class="media__picker-button ${active === 'all' ? 'media__picker-button--active' : ''}" data-button-type="all">All</button> 2413 ${counts.videos > 0 ? `<button type="button" class="media__picker-button ${active === 'videos' ? 'media__picker-button--active' : ''}" data-button-type="videos">Videos</button>` : ''} 2414 ${counts.celebs > 0 ? `<button type="button" class="media__picker-button ${active === 'celebs' ? 'media__picker-button--active' : ''}" data-button-type="celebs">Celebs</button>` : ''} 2415 ${counts.movies > 0 ? `<button type="button" class="media__picker-button ${active === 'movies' ? 'media__picker-button--active' : ''}" data-button-type="movies">Movies</button>` : ''} 2416 ${counts.stories > 0 ? `<button type="button" class="media__picker-button ${active === 'stories' ? 'media__picker-button--active' : ''}" data-button-type="stories">Stories</button>` : ''} 2417 </div> 2418 `; 2419 } 2420 2421 function computeCounts(results) { 2422 return { 2423 celebs: results.celebs.length, 2424 movies: results.movies.length, 2425 stories: results.stories.length, 2426 videos: results.videos.length 2427 }; 2428 } 2429 2430 function hasAnyResults(results) { 2431 return !!( 2432 (results?.celebs && results.celebs.length) || 2433 (results?.movies && results.movies.length) || 2434 (results?.stories && results.stories.length) || 2435 (results?.videos && results.videos.length) 2436 ); 2437 } 2438 2439 function renderResultsDesktop(results) { 2440 if (!hasAnyResults(results)) { hideDesktopDropdown(); return; } 2441 lastResultsDesktop = results; 2442 const dd = ensureDesktopDropdown(); 2443 const counts = computeCounts(results); 2444 2445 if (activeCategoryDesktop === 'all') { 2446 dd.innerHTML = ` 2447 ${buildTabs(counts, activeCategoryDesktop)} 2448 <div class="explore-groups"> 2449 ${results.celebs.length ? ` 2450 <div class="explore-group" data-type="celebs"> 2451 <h3 class="explore-title">Celebs</h3> 2452 <ul class="explore-list"> 2453 ${results.celebs.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2454 </ul> 2455 </div>` : ``} 2456 ${results.movies.length ? ` 2457 <div class="explore-group" data-type="movies"> 2458 <h3 class="explore-title">Movies</h3> 2459 <ul class="explore-list"> 2460 ${results.movies.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2461 </ul> 2462 </div>` : ``} 2463 ${results.stories.length ? ` 2464 <div class="explore-group" data-type="stories"> 2465 <h3 class="explore-title">Stories</h3> 2466 <ul class="explore-list"> 2467 ${results.stories.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2468 </ul> 2469 </div>` : ``} 2470 ${results.videos.length ? ` 2471 <div class="explore-group" data-type="videos"> 2472 <h3 class="explore-title">Videos</h3> 2473 <ul class="explore-list"> 2474 ${results.videos.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2475 </ul> 2476 </div>` : ``} 2477 </div> 2478 `; 2479 } else { 2480 const set = results[activeCategoryDesktop] || []; 2481 const listHTML = set.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("") || `<li>No results</li>`; 2482 2483 dd.innerHTML = ` 2484 ${buildTabs(counts, activeCategoryDesktop)} 2485 <ul class="explore-list results-list">${listHTML}</ul> 2486 `; 2487 } 2488 2489 dd.style.display = "block"; 2490 2491 dd.querySelectorAll('.media__picker-button').forEach(btn => { 2492 btn.addEventListener('click', async (e) => { 2493 e.preventDefault(); 2494 e.stopPropagation(); 2495 2496 const next = btn.getAttribute('data-button-type'); 2497 activeCategoryDesktop = next; 2498 2499 if (next === 'all') { 2500 const bucket = getCacheForQuery(lastQueryDesktop); 2501 const base = bucket?.initial || lastResultsDesktop || { celebs:[], movies:[], stories:[], videos:[] }; 2502 renderResultsDesktop(base); 2503 return; 2504 } 2505 2506 const merged = await ensureTypeData(lastQueryDesktop, next); 2507 renderResultsDesktop(merged || lastResultsDesktop); 2508 }); 2509 }); 2510 } 2511 2512 function renderResultsMobile(results) { 2513 if (!hasAnyResults(results)) { if (suggestionsContainer) suggestionsContainer.style.display = "none"; return; } 2514 lastResultsMobile = results; 2515 const host = suggestionsContainer?.querySelector('.results-host'); 2516 if (!host) return; 2517 2518 const counts = computeCounts(results); 2519 2520 if (activeCategoryMobile === 'all') { 2521 host.innerHTML = ` 2522 ${buildTabs(counts, activeCategoryMobile)} 2523 <div class="explore-groups"> 2524 ${results.celebs.length ? ` 2525 <div class="explore-group" data-type="celebs"> 2526 <h3 class="explore-title">Celebs</h3> 2527 <ul class="suggestions-list"> 2528 ${results.celebs.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2529 </ul> 2530 </div>` : ``} 2531 ${results.movies.length ? ` 2532 <div class="explore-group" data-type="mov
2532ies"> 2533 <h3 class="explore-title">Movies</h3> 2534 <ul class="suggestions-list"> 2535 ${results.movies.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2536 </ul> 2537 </div>` : ``} 2538 ${results.stories.length ? ` 2539 <div class="explore-group" data-type="stories"> 2540 <h3 class="explore-title">Stories</h3> 2541 <ul class="suggestions-list"> 2542 ${results.stories.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2543 </ul> 2544 </div>` : ``} 2545 ${results.videos.length ? ` 2546 <div class="explore-group" data-type="videos"> 2547 <h3 class="explore-title">Videos</h3> 2548 <ul class="suggestions-list"> 2549 ${results.videos.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("")} 2550 </ul> 2551 </div>` : ``} 2552 </div> 2553 `; 2554 } else { 2555 const set = results[activeCategoryMobile] || []; 2556 host.innerHTML = ` 2557 ${buildTabs(counts, activeCategoryMobile)} 2558 <ul class="suggestions-list results-list"> 2559 ${set.map(({url, text}) => `<li><a href="${url}">${text}</a></li>`).join("") || `<li>No results</li>`} 2560 </ul> 2561 `; 2562 } 2563 2564 suggestionsContainer.style.display = "block"; 2565 2566 host.querySelectorAll('.media__picker-button').forEach(btn => { 2567 btn.addEventListener('click', async (e) => { 2568 e.preventDefault(); 2569 e.stopPropagation(); 2570 2571 const next = btn.getAttribute('data-button-type'); 2572 activeCategoryMobile = next; 2573 2574 if (next === 'all') { 2575 const bucket = getCacheForQuery(lastQueryMobile); 2576 const base = bucket?.initial || lastResultsMobile || { celebs:[], movies:[], stories:[], videos:[] }; 2577 renderResultsMobile(base); 2578 return; 2579 } 2580 2581 const merged = await ensureTypeData(lastQueryMobile, next); 2582 renderResultsMobile(merged || lastResultsMobile); 2583 }); 2584 }); 2585 2586 const recentC = document.querySelector(".recent-searches-container"); 2587 const celebsC = document.querySelector(".trending-celebs-container"); 2588 if (recentC) recentC.style.display = "none"; 2589 if (trendingContainer) trendingContainer.style.display = "none"; 2590 if (celebsC) celebsC.style.display = "none"; 2591 } 2592 2593 /* How long a failed suggestion tier is skipped for. Long enough that a 2594 * burst of typing does not re-pay the same timeout per character, short 2595 * enough that a tier coming back is picked up within a few seconds. */ 2596 const SUGGEST_TIER_COOLDOWN_MS = 15000; 2597 const suggestTierCooldown = Object.create(null); 2598 2599 async function fetchSuggestionsPayload(query, opts = {}) { 2600 const { limit, type, offset, signal } = opts; 2601 2602 const resolvedSexuality = (typeof sexuality !== 'undefined' && sexuality) 2603 ? sexuality 2604 : (varValues?.sexuality || ''); 2605 const sexualityParam = resolvedSexuality 2606 ? `&sexuality=${encodeURIComponent(resolvedSexuality)}` 2607 : ''; 2608 2609 const typeParam = type ? `&type=${encodeURIComponent(type)}` : ''; 2610 const limitParam = Number.isFinite(limit) ? `&limit=${limit}` : ''; 2611 const offsetParam = Number.isFinite(offset) ? `&offset=${offset}` : ''; 2612 2613 const path = 2614 `/exp/search-suggestions` + 2615 `?q=${encodeURIComponent(query)}` + 2616 `&gender=${encodeURIComponent(varValues.gender)}` + 2617 `${sexualityParam}${typeParam}${limitParam}${offsetParam}`; 2618 2619 /* 2620 * THE THREE TIERS ARE THREE HOSTS, not three branches inside one. 2621 * 2622 * NEON_BASE mainNeon Neon 2623 * PRIMARY_BASE main Turso 2624 * D1_BASE the Worker D1 2625 * 2626 * Same shape as SearchPage.jsx's SEARCH_TIERS, and for the same reason: 2627 * each host is a separate failure domain, so a dead one surfaces as a 2628 * failure this loop can walk past instead of being absorbed server-side 2629 * into a slower answer that looks healthy. functions/index.js says the 2630 * same thing from the other end. 2631 *
2632 * BUDGETS ARE TIGHTER THAN /search's 2000/1500/3000, deliberately. 2633 * A search runs once; suggestions run on every keystroke, so a tier that 2634 * is merely slow costs the user a laggy dropdown on every character 2635 * rather than one slow page. Two things bound the damage: adaptiveDebounce 2636 * aborts the in-flight request when the next character arrives, and a 2637 * tier that times out is skipped for that keystroke only, not remembered. 2638 * 2639 * NEON GETS THE MOST OF THE THREE because it is the one host the page has 2640 * not already talked to. PRIMARY_BASE is warm by the time anyone types - 2641 * page load has already called /search-token and /most-searched-queries 2642 * against it - so it pays neither the TLS handshake nor the CORS preflight 2643 * that x-st/x-sid force on a first request. mainNeon pays both on the 2644 * first keystroke of a page view. Measured for /search on 2026-09-05: 2645 * handshake 326-442ms, preflight 233ms, both INSIDE this budget. 2646 * 2647 * REVISIT IF preconnect LANDS. A <link rel="preconnect"> for the mainNeon 2648 * host would move the handshake into page load and let neon come down to 2649 * ~700ms. That is the better fix; 1200 is sized for its absence. 2650 * 2651 * A tier whose base is missing is SKIPPED, not attempted, so this file 2652 * keeps working if NEON_BASE is ever removed. 2653 * 2654 * ON AN EDGE HOST THE FIRST TIER IS THE EDGE, and it replaces neon rather 2655 * than joining the walk in front of it; see suggestFirstTier. 2656 */ 2657 const SUGGEST_TIERS = [ 2658 suggestFirstTier(), 2659 { name: 'turso', base: typeof PRIMARY_BASE !== 'undefined' ? PRIMARY_BASE : null, timeout: 1200 }, 2660 { name: 'd1', base: typeof D1_BASE !== 'undefined' ? D1_BASE : null, timeout: 2000 }, 2661 ]; 2662 2663 /* 2664 * One signal per attempt, firing on either the caller's abort or this 2665 * tier's timeout, so a keystroke cancels in flight and a slow host still 2666 * gets cut off. `timedOut` tells them apart afterwards: a caller abort 2667 * ends the walk, a timeout moves to the next tier. 2668 */ 2669 function tierSignal(ms) { 2670 const ctl = new AbortController(); 2671 const state = { timedOut: false }; 2672 const onAbort = () => ctl.abort(); 2673 2674 if (signal) { 2675 if (signal.aborted) ctl.abort(); 2676 else signal.addEventListener('abort', onAbort, { once: true }); 2677 } 2678 const timer = setTimeout(() => { state.timedOut = true; ctl.abort(); }, ms); 2679 2680 state.signal = ctl.signal; 2681 state.done = () => { 2682 clearTimeout(timer); 2683 if (signal) signal.removeEventListener('abort', onAbort); 2684 }; 2685 return state; 2686 } 2687 2688 async function fetchFromTier(base, timeout, tokenHost) { 2689 /* 2690 * THE TOKEN IS FETCHED BEFORE THE TIMER STARTS, and that ordering is 2691 * the fix for a bug, not a stylistic choice. With the timer started 2692 * first, a cold mint - bounded separately at TOKEN_MINT_TIMEOUT_MS, 2693 * 1000ms - ate almost the whole tier budget, so the first keystroke 2694 * of a page view would time out on Neon and fall to Turso every time. 2695 * That reads as 'Neon is slow' and is really 'Neon was never reached'. 2696 * The mint carries its own timeout, so nothing here is unbounded. 2697 */ 2698 const { token, sid } = await getSearchTokenFor(tokenHost); 2699 2700 const tier = tierSignal(timeout); 2701 try { 2702 const json = await fetchSuggestionsFrom(base, path, { token, sid, signal: tier.signal }); 2703 return normalizeResultsPayload(json); 2704 } finally { 2705 tier.done(); 2706 } 2707 } 2708 2709 async function tryFetch(base, timeout, tokenBase = base) { 2710 const tokenHost = tokenBase.replace(/\/$/, ''); 2711 try { 2712 return await fetchFromTier(base, timeout, tokenHost); 2713 } catch (err) { 2714 /* 2715 * AZ-269: on AUTH_401, purge the token and retry once. TokenManager trusts the 2716 * browser's clock, so a slow one replays an expired token on every tier until 2717 * reload. A refusal that isn't expiry survives the re-mint and fails as before. 2718 */ 2719 if (!String(err?.message).startsWith('AUTH_401') || signal?.aborted) throw err; 2720 invalidateSearchTokenFor(tokenHost); 2721 return fetchFromTier(base, timeout, tokenHost); 2722 } 2723 } 2724 2725 const failures = []; 2726 for (const { name, base, timeout, tokenBase } of SUGGEST_TIERS) { 2727 if (!base) continue; 2728 if (signal?.aborted) throw new DOMException('Aborted', 'AbortError'); 2729 2730 /* 2731 * A tier that just failed is skipped rather than retried. Without
2732 * this a stalled mainNeon costs its full budget on EVERY keystroke - 2733 * the walk has no memory, so each character pays the same timeout 2734 * again before reaching a host that works. One failure parks the tier 2735 * for SUGGEST_TIER_COOLDOWN_MS and the dropdown stays responsive 2736 * through an outage instead of degrading with every letter typed. 2737 */ 2738 if (suggestTierCooldown[name] > Date.now()) { 2739 failures.push(name + ': skipped, cooling down'); 2740 continue; 2741 } 2742 2743 try { 2744 return await tryFetch(base, timeout, tokenBase); 2745 } catch (err) { 2746 /* The user typed. Stop the whole walk - the answer is already 2747 * stale and the next keystroke has its own request in flight. 2748 * Not a tier fault, so it must not trip the cooldown. */ 2749 if (signal?.aborted) throw err; 2750 /* An edge failure takes the edge off for the page view, as it 2751 * does for the trending strip, so after the cooldown the first 2752 * tier is plain neon rather than the proxy again. A rejected 2753 * token is the exception: it is not the proxy's fault, and it 2754 * would be rejected the same way on mainNeon directly. */ 2755 if (name === 'edge' && !String(err?.message).startsWith('AUTH_401')) { 2756 disableEdgeRead(err); 2757 } 2758 suggestTierCooldown[name] = Date.now() + SUGGEST_TIER_COOLDOWN_MS; 2759 failures.push(name + ': ' + (err?.message || err)); 2760 } 2761 } 2762 2763 /* 2764 * Every host is gone, or every one is cooling down. This was an empty 2765 * catch returning undefined, which hasAnyResults() read as 'no matches' 2766 * and the dropdown simply hid - so a total outage was indistinguishable 2767 * from a query with no content, in the UI and in the telemetry both. 2768 * 2769 * The payload is MARKED, and that matters beyond logging: ensureTypeData 2770 * caches whatever comes back as the complete list for that type, so an 2771 * unmarked empty answer would pin the initial five rows as the full set 2772 * and 'more' would never load again for that query, for the rest of the 2773 * page view. A transient outage would become a permanent one. 2774 */ 2775 console.error('search-suggestions failed on every tier:', failures.join(' | ')); 2776 const dead = normalizeResultsPayload({ data: [] }); 2777 dead.__failed = true; 2778 return dead; 2779 } 2780 2781 async function handleDesktopFormClick(e) { 2782 if (e.target.closest(".search-suggestions-dropdown")) return; 2783 2784 searchInputDesktop.focus(); 2785 2786 const val = searchInputDesktop.value.trim(); 2787 const key = getCacheKey(val); 2788 const bucket = getCacheForQuery(key); 2789 2790 if (val.length >= 2) { 2791 if (bucket.initial) { 2792 lastQueryDesktop = key; 2793 activeCategoryDesktop = 'all'; 2794 lastResultsDesktop = bucket.initial; 2795 renderResultsDesktop(bucket.initial); 2796 return; 2797 } 2798 await renderDesktopByQuery(val); 2799 } else { 2800 if (dropdownDesktop && dropdownDesktop.style.display !== "none" && !lastResultsDesktop) return; 2801 await renderExploreDesktop(); 2802 } 2803 } 2804 2805 searchFormDesktop.addEventListener("click", handleDesktopFormClick); 2806 2807 async function renderDesktopByQuery(query, opts = {}) { 2808 const { force = false } = opts; 2809 try { 2810 const key = getCacheKey(query); 2811 lastQueryDesktop = key; 2812 2813 const bucket = getCacheForQuery(key); 2814 2815 if (!force && bucket.initial) { 2816 activeCategoryDesktop = 'all'; 2817 lastResultsDesktop = bucket.initial; 2818 renderResultsDesktop(bucket.initial); 2819 return; 2820 } 2821 2822 const results = await fetchSuggestionsPayload(query, { limit: 5 }); 2823 if (!hasAnyResults(results)) { 2824 hideDesktopDropdown(); 2825 lastResultsDesktop = null; 2826 return; 2827 } 2828 2829 bucket.initial = results; 2830 2831 activeCategoryDesktop = 'all'; 2832 lastResultsDesktop = results; 2833 renderResultsDesktop(results); 2834 } catch (e) { 2835 console.error(e); 2836 hideDesktopDropdown(); 2837 } 2838 } 2839 2840 async function ensureTypeData(queryKey, type) { 2841 const bucket = getCacheForQuery(queryKey); 2842 if (!bucket.initial) return null; 2843 2844 if (bucket.fullByType[type] && bucket.fullByType[type].length > 0) { 2845 return { 2846 celebs : bucket.fullByType.celebs || bucket.initial.celebs, 2847 movies : bucket.fullByType.movies || bucket.initial.movies, 2848 stories: bucket.fullByType.stories || bucket.initial.stories, 2849 videos : bucket.fullByType.videos || bucket.initial.videos 2850 }; 2851 } 2852 2853 const existing = (bucket.initial[type] || []); 2854 const offset = existing.length; 2855 try { 2856 const more = await fetchSuggestionsPayload(queryKey, { type, offset }); 2857 2858 /* 2859 * A total tier failure must not be cached as the complete list. 2860 * fetchSuggestionsPayload marks that case; without the check, an 2861 * outage lasting one request would pin the initial rows as the full 2862 * set for this type, and 'more' would never load again for this 2863 * query for the rest of the page view - a transient failure turned 2864 * permanent. Returning bucket.initial leaves fullByType unset, so the 2865 * next attempt retries. 2866 */ 2867 if (more.__failed) return bucket.initial; 2868 2869 const appended = (more[type] && more[type].length) ? existing.concat(more[type]) : existing.slice(); 2870 2871 bucket.fullByType[type] = appended; 2872 2873 return { 2874 celebs : type === 'celebs' ? bucket.fullByType.celebs : bucket.initial.celebs, 2875 movies : type === 'movies' ? bucket.fullByType.movies : bucket.initial.movies, 2876 stories: type === 'stories' ? bucket.fullByType.stories : bucket.initial.stories, 2877 videos : type === 'videos' ? bucket.fullByType.videos : bucket.initial.videos 2878 }; 2879 } catch (err) { 2880 console.error('ensureTypeData error:', err); 2881 return bucket.initial; 2882 } 2883 } 2884 2885 const remoteSearchDesktop = adaptiveDebounce(async (q, { signal }) => {
2886 const key = getCacheKey(q); 2887 lastQueryDesktop = key; 2888 2889 const results = await fetchSuggestionsPayload(q, { limit: 5, signal }); 2890 if (!hasAnyResults(results)) { 2891 hideDesktopDropdown(); 2892 lastResultsDesktop = null; 2893 return; 2894 } 2895 2896 const bucket = getCacheForQuery(key); 2897 bucket.initial = results; 2898 2899 activeCategoryDesktop = 'all'; 2900 lastResultsDesktop = results; 2901 renderResultsDesktop(results); 2902 }, { 2903 leading: false, 2904 min: 100, 2905 max: 600, 2906 k: 1.4, 2907 shortFloor: 260, 2908 fastThreshold: 140, 2909 fastFloor: 360, 2910 idleReset: 1000, 2911 alpha: 0.4 2912 }); 2913 2914 const remoteSearchMobile = adaptiveDebounce(async (q, { signal }) => { 2915 await ensureMobileExploreContainers(); 2916 2917 const key = getCacheKey(q); 2918 lastQueryMobile = key; 2919 2920 const results = await fetchSuggestionsPayload(q, { limit: 5, signal }); 2921 if (!hasAnyResults(results)) { 2922 if (suggestionsContainer) suggestionsContainer.style.display = "none"; 2923 lastResultsMobile = null; 2924 await renderExploreMobile(); 2925 return; 2926 } 2927 2928 const bucket = getCacheForQuery(key); 2929 bucket.initial = results; 2930 2931 activeCategoryMobile = 'all'; 2932 lastResultsMobile = results; 2933 renderResultsMobile(results); 2934 }, {
2935 leading: false, 2936 min: 100, 2937 max: 600, 2938 k: 1.4, 2939 shortFloor: 260, 2940 fastThreshold: 140, 2941 fastFloor: 360, 2942 idleReset: 1000, 2943 alpha: 0.4 2944 }); 2945 2946 searchInputDesktop.addEventListener("input", async function() { 2947 const query = this.value.trim(); 2948 if (query.length < 2) { 2949 remoteSearchDesktop.cancel(); 2950 await renderExploreDesktop(); 2951 return; 2952 } 2953 remoteSearchDesktop(query); 2954 }); 2955 2956 searchInput.addEventListener("input", async function() { 2957 const query = this.value.trim(); 2958 if (query.length < 2) { 2959 remoteSearchMobile.cancel(); 2960 await renderExploreMobile(); 2961 return; 2962 } 2963 remoteSearchMobile(query); 2964 }); 2965 2966 searchInputDesktop.addEventListener("keydown", e => { if (e.key === "Enter") remoteSearchDesktop.flush(); }); 2967 searchInput.addEventListener("keydown", e => { if (e.key === "Enter") remoteSearchMobile.flush(); }); 2968 searchInputDesktop.addEventListener("blur", () => remoteSearchDesktop.flush()); 2969 searchInput.addEventListener("blur", () => remoteSearchMobile.flush()); 2970 2971 document.addEventListener("click", e => { 2972 const inDropdown = e.target.closest(".search-suggestions-dropdown"); 2973 const inSearchBar = e.target.closest(".search-bar"); 2974 if (dropdownDesktop && !inDropdown && !inSearchBar) { 2975 hideDesktopDropdown(); 2976 } 2977 }); 2978 2979 searchToggle.addEventListener("click", async e => { 2980 e.preventDefault(); 2981 if (window.innerWidth <= 480) { 2982 const isActive = searchContainer.classList.contains("active"); 2983 searchContainer.classList.toggle("active", !isActive); 2984 document.body.style.overflow = isActive ? "auto" : "hidden"; 2985 if (!isActive) { 2986 // searchInput.focus(); 2987 await renderExploreMobile(); 2988 } 2989 } 2990 }); 2991 2992 searchCloseBtn.addEventListener("click", () => { 2993 remoteSearchMobile.cancel(); 2994 searchContainer.classList.remove("active"); 2995 if (suggestionsContainer) suggestionsContainer.style.display = "none"; 2996 document.body.style.overflow = "auto"; 2997 }); 2998 2999 window.addEventListener("resize", () => { 3000 if (window.innerWidth > 480 && searchContainer.classList.contains("active")) { 3001 searchContainer.classList.remove("active"); 3002 document.body.style.overflow = "auto"; 3003 } 3004 }); 3005 3006 // region Sexuality Dropdown 3007 3008 setupSexualityDropdown(document.querySelector('[data-sexuality-dropdown]')) 3009 3010 function setupSexualityDropdown(sexualityDropdown) { 3011 if (!sexualityDropdown) return; 3012 3013 // contains the selected option text and svg 3014 const sexualityToggle = sexualityDropdown.querySelector('.sexuality-dropdown__toggle'); 3015 const mobileSexualitySelector = document.querySelector('.mobile-sexuality-selector'); 3016 3017 // the dropdown menu options, each of the form <a href> <svg></svg> <span>option text</span> </a> 3018 const sexualityMenuItems = sexualityDropdown.querySelectorAll('.sexuality-dropdown__menu .mobile-sexuality-category-dropdown-item'); 3019 const mobileDropdownItems = document.querySelectorAll('.mobile-sexuality-category-content .mobile-sexuality-category-dropdown-item[href]'); 3020 3021 // normalize path function 3022 const normalizePath = path => path !== '/' ? path.replace(/\/+$/, '').split('?')[0] : path; 3023 3024 function updateSexualitySelection(category) { 3025 const desktopItem = [...sexualityMenuItems].find(item=>item.dataset.category === category); 3026 const mobileItem = [...mobileDropdownItems].find(item=>item.dataset.category === category); 3027 if (!desktopItem || !mobileItem) return; 3028 3029 const selectedItems = [desktopItem, mobileItem]; 3030 [sexualityToggle, mobileSexualitySelector].forEach((item, index) => { 3031 const svg = selectedItems[index].querySelector('svg'); 3032 if (svg) item.querySelector('svg').outerHTML = svg.outerHTML 3033 let spans = item.querySelectorAll('span'); 3034 spans[spans.length - 1].textContent = category; 3035 }); 3036 3037 [...sexualityMenuItems, ...mobileDropdownItems].forEac
3037h(item => item.classList.toggle('active', item.dataset.category === category)); 3038 localStorage.setItem('sexuality', category); 3039 } 3040 3041 function getRedirectPath(cat, currentPath) { 3042 const basePath = normalizePath(currentPath); 3043 // currently on / -> return expected / url 3044 if (basePath in pathToCategory || basePath === '/') return categoryToPath[cat]; 3045 else if (/\/browse(?:\/|$)/.test(basePath)) { 3046 const p = basePath.split('/'); 3047 const tail = [...p.slice('/' + p[1] in pathToCategory ? 2 : 1, p.length - 1)]; 3048 tail.splice(0, 0, categoryToPath[cat] === '/' ? '' : categoryToPath[cat]); 3049 const currentCat = '/' + p[1] in pathToCategory ? pathToCategory['/' + p[1]] : pathToCategory['/'] 3050 currentCat === cat ? tail.push(p[p.length - 1]) : tail.push('1.html'); 3051 return tail.join('/') || '/'; 3052 } 3053 return basePath; 3054 } 3055 3056 3057 const currentPath = window.location.pathname; 3058 const storedCategory = localStorage.getItem('sexuality'); 3059 const pathToCategory = {}; 3060 const categoryToPath = {}; 3061 const valid = []; 3062 sexualityMenuItems.forEach(item => { 3063 const href = item.getAttribute('href'); 3064 const cat = item.dataset.category; 3065 pathToCategory[href] = cat; 3066 categoryToPath[cat] = href; 3067 valid.push(cat); 3068 }); 3069 3070 const selectedCategory = valid.includes(storedCategory) ? storedCategory : valid[0] 3071 const expectedPath = getRedirectPath(storedCategory, window.location.pathname); 3072 3073 sexualityDropdown.classList.add('loading'); 3074 valid.includes(storedCategory) && normalizePath(currentPath) !== normalizePath(expectedPath) 3075 ? window.location.href = expectedPath : null; 3076 3077 updateSexualitySelection(selectedCategory); 3078 sexualityDropdown.classList.remove('loading'); 3079 3080 sexualityToggle.addEventListener('click', () => { 3081 sexualityDropdown.classList.toggle('open'); 3082 }); 3083 3084 [...sexualityMenuItems, ...mobileDropdownItems].forEach(item => { 3085 item.addEventListener('click', e => { 3086 e.preventDefault(); 3087 const cat = item.dataset.category; 3088 updateSexualitySelection(cat); 3089 if (sexualityDropdown.classList.contains('open')) sexualityDropdown.classList.remove('open'); 3090 const path = window.location.pathname; 3091 path === '/search.html' ? window.location.reload() : window.location.href = getRedirectPath(cat, path); 3092 }); 3093 }); 3094 3095 document.addEventListener('click', e => { 3096 if (sexualityDropdown.classList.contains('open') && !sexualityDropdown.contains(e.target)) { 3097 sexualityDropdown.classList.remove('open'); 3098 } 3099 }); 3100 3101 mobileSexualitySelector.addEventListener('click', e => { 3102 e.preventDefault(); 3103 const mobileSexualityDropdown = document.querySelector('.mobile-sexuality-category-dropdown-content'); 3104 const isOpen = mobileSexualityDropdown.classList.contains('open'); 3105 mobileSexualityDropdown.classList.toggle('open'); 3106 mobileSexualitySelector.querySelector('.dropdown-arrow')?.classList.toggle('rotated', !isOpen); 3107 }); 3108 3109 }// endregion 3110 3111 3112 // **Websites Dropdown** 3113 const azWebsitesToggle = document.querySelector('.hamburger-a-z-websites-toggle'); 3114 const azWebsitesDropdown = document.querySelector('.hamburger-a-z-websites-dropdown-content'); 3115 3116 if (azWebsitesToggle && azWebsitesDropdown) { 3117 azWebsitesToggle.addEventListener('click', e => { 3118 e.preventDefault(); 3119 const isOpen = azWebsitesDropdown.classList.contains('open'); 3120 azWebsitesDropdown.classList.toggle('open'); 3121 azWebsitesToggle.querySelector('.dropdown-arrow')?.classList.toggle('rotated', !isOpen); 3122 }); 3123 } 3124 3125 // **Browse Dropdown** 3126 const browseToggle = document.querySelector('.browse-toggle'); 3127 const browseDropdown = document.querySelector('.browse-dropdown-content'); 3128 3129 browseToggle.addEventListener('click', e => { 3130 e.preventDefault(); 3131 const isOpen = browseDropdown.classList.contains('open'); 3132 browseDropdown.classList.toggle('open'); 3133 browseToggle.querySelector('.dropdown-arrow')?.classList.toggle('rotated', !isOpen); 3134 }); 3135 3136 // **Direct Link Handling** 3137 function shuffleArray(arr) { 3138 for (let i = arr.length - 1; i > 0; i--) { 3139 const j = Math.floor(Math.random() * (i + 1)); 3140 [arr[i], arr[j]] = [arr[j], arr[i]]; 3141 } 3142 return arr; 3143 } 3144 3145 fetch(`https://${varValues.cdn}/js/dating.json`) 3146 .then(response => { 3147 if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); 3148 return response.json(); 3149 }) 3150 .then(data => { 3151 const {baseUrl, ads} = data; 3152 3153 const commonId = ads[0].id; 3154 const items = shuffleArray(ads[0].items); 3155 3156 const aiId = ads[1].id; 3157 const aiItems = shuffleArray(ads[1].items); 3158 3159 const { text, sub } = items[0]; 3160 const {text: aiText } = aiItems[0]; 3161 3162 ['direct-link-desktop', 'direct-link-navbar', 'direct-link-mobile', 'direct-link-footer', 'direct-link-video', 'direct-link-mobile-2'].forEach(id => { 3163 const el = document.getElementById(id); 3164 if (el) { 3165 if (id === "direct-link-mobile") { 3166 el.innerHTML = `<img src="https://cdn.aznude.com/images/svgs/water.svg" width="26" height="26" alt=""><span>${aiText}</span>`;
3167 } else if (id === "direct-link-mobile-2"){ 3168 el.innerHTML = `<img src="https://cdn.aznude.com/images/svgs/water.svg" width="26" height="26" alt=""><span>${text}</span>`; 3169 } else if (id === "direct-link-navbar"){ 3170 const span = el.querySelector('span'); 3171 if (span) span.textContent = aiText; else el.textContent = aiText; 3172 } else { 3173 const span = el.querySelector('span'); 3174 if (span) span.textContent = text; else el.textContent = text; 3175 } 3176 3177 if(id === "direct-link-navbar" || id === "direct-link-mobile"){ 3178 el.setAttribute('href', `${baseUrl}?z=${aiId}`); 3179 } else { 3180 el.setAttribute('href', `${baseUrl}?z=${commonId}&sub=${sub}`); 3181 el.setAttribute('sub', sub); 3182 } 3183 } 3184 }); 3185 }) 3186 .catch(error => { 3187 3188 }); 3189 3190 // **Country Detection - Global** 3191 async function detectAndStoreCountry() { 3192 const countryCookie = getCookie("countryData"); 3193 if (countryCookie) { 3194 try { 3195 return JSON.parse(countryCookie); 3196 } catch (e) { 3197 console.error('Error parsing country cookie:', e); 3198 } 3199 } 3200 3201 try { 3202 const response = await fetch('https://www.cloudflare.com/cdn-cgi/trace'); 3203 const text = await response.text(); 3204 const data = {}; 3205 text.split('\n').forEach(line => { 3206 const [key, value] = line.split('='); 3207 if (key && value) data[key] = value; 3208 }); 3209 const countryCode = data.loc; 3210 const countriesResp = await fetch(`https://${varValues.cdn}/js/countries.json`); 3211 const countries = await countriesResp.json(); 3212 const country = countries.find(c => c.code === countryCode); 3213 const countryName = country ? country.name : "Unknown Country"; 3214 const countryData = { country_name: countryName, country_code: countryCode }; 3215 setCookie("countryData", JSON.stringify(countryData), 1); 3216 return countryData; 3217 } catch (error) { 3218 console.error('Error detecting country:', error); 3219 return null; 3220 } 3221 } 3222 3223 detectAndStoreCountry(); 3224 3225 if(document.querySelector('.location-bar')){ 3226 detectAndStoreCountry().then(countryData => { 3227 if (countryData && countryData.country_name && countryData.country_code) { 3228 updateCountryInfo(countryData.country_name, countryData.country_code); 3229 } else { 3230 document.querySelector('.location-bar').style.display = "none"; 3231 } 3232 }).catch(error => { 3233 console.error('Error in location bar country detection:', error); 3234 document.querySelector('.location-bar').style.display = "none"; 3235 }); 3236 3237 function updateCountryInfo(country, countryCode) { 3238 const countryText = document.getElementById('country-text'); 3239 const locationIconContainer = countryText.querySelector('.location-icon-container'); 3240 countryText.innerHTML = ''; 3241 countryText.appendChild(locationIconContainer); 3242 countryText.insertAdjacentHTML('beforeend', `<span class="country-text">${varValues.celeb_names} from ${country} </span>`); 3243 countryText.href = `/browse/${varValues.celeb_names.toLowerCase()}/country/${countryCode.toLowerCase()}/1.html`; 3244 countryText.insertAdjacentHTML('beforeend', `<img src="https://cdn.aznude.com/images/svgs/flags/${countryCode.toLowerCase()}.svg" alt="${country} flag" style="margin-left: 5px; vertical-align: middle; height: 13px;">`); 3245 } 3246 } 3247 3248 function updateCountryInfo(country, countryCode) { 3249 const countryText = document.getElementById('country-text'); 3250 const locationIconContainer = countryText.querySelector('.location-icon-container'); 3251 countryText.innerHTML = ''; 3252 countryText.appendChild(locationIconContainer); 3253 countryText.insertAdjacentHTML('beforeend', `<span class="country-text">${varValues.celeb_names} from ${country} </span>`); 3254 countryText.href = `/browse/${varValues.celeb_names.toLowerCase()}/country/${countryCode.toLowerCase()}/1.html`; 3255 countryText.insertAdjacentHTML('beforeend', `<img src="https://cdn.aznude.com/images/svgs/flags/${countryCode.toLowerCase()}.svg" alt="${country} flag" style="margin-left: 5px; vertical-align: middle; height: 13px;">`); 3256 } 3257}); 3258 3259window.addEventListener('load', async () => { 3260 if (getCookie('cookieConsent') === 'true') return; 3261 3262 try { 3263 const traceResponse = await fetch('https://www.cloudflare.com/cdn-cgi/trace'); 3264 const traceText = await traceResponse.text(); 3265 const data = {}; 3266 traceText.split('\n').forEach(line => { 3267 const [key, value] = line.split('='); 3268 if (key && value) data[key] = value; 3269 }); 3270 const countryCode = data.loc; 3271 const euResponse = await fetch(`https://${varValues.cdn}/js/eu.json`); 3272 const euCountries = await euResponse.json(); 3273 3274 if (euCountries.includes(countryCode)) { 3275 const bar = document.createElement('div'); 3276 bar.className = 'cookie-bar'; 3277 bar.innerHTML = ` 3278 <div class="cookie-content"> 3279 <p>${varValues.title} uses cookies. <a href="/privacy.html" rel="noopener" target="_blank" tabindex="1">Learn more</a></p> 3280 <button class="got-it-btn">Got it!</button> 3281 </div> 3282 `; 3283 document.body.appendChild(bar); 3284 3285 setTimeout(() => { 3286 bar.classList.add('visible'); 3287 }, 10); 3288 3289 const gotItBtn = bar.querySelector('.got-it-btn'); 3290 gotItBtn.addEventListener('click', () => { 3291 bar.classList.remove('visible');
3292 setCookie('cookieConsent', 'true', 365); 3293 setTimeout(() => { 3294 bar.remove(); 3295 }, 500); 3296 }); 3297 } 3298 } catch (error) { 3299 console.error('Error checking cookie consent:', error); 3300 } 3301}); 3302 3303const getKeywords = function() { 3304 let keywords_data = $("#data-keywords").attr('data'); 3305 let content = $("[name=keywords]").attr('content'); 3306 let title = $('title').text(); 3307 let description = $('[name=description]').attr('content'); 3308 let combined_tags = []; 3309 if (keywords_data) combined_tags.push(keywords_data); 3310 else { 3311 if (content) combined_tags.push(content); 3312 if (title) combined_tags.push(title); 3313 if (description) combined_tags.push(description); 3314 } 3315 return combined_tags.join(', '); 3316} 3317 3318// What is this? 3319const pushTags = function() { 3320 let combined_tags = getKeywords(); 3321 if (combined_tags.length > 0 && combined_tags !== '') { 3322 let i, dataElements; 3323 dataElements = document.getElementsByClassName("adsbyexoclick"); 3324 for (i = 0; i<dataElements.length; i++) { 3325 dataElements[i].setAttribute("data-keywords", combined_tags); 3326 } 3327 dataElements = document.getElementsByClassName("eas6a97888e"); 3328 for (i = 0; i<dataElements.length; i++) { 3329 dataElements[i].setAttribute("data-keywords", combined_tags); 3330 } 3331 dataElements = document.getElementsByClassName("eas6a97888e37"); 3332 for (i = 0; i<dataElements.length; i++) { 3333 dataElements[i].setAttribute("data-keywords", combined_tags); 3334 } 3335 dataElements = document.querySelectorAll('[data-zoneid]'); 3336 for (i = 0; i<dataElements.length; i++) { 3337 const value = dataElements[i].getAttribute('data-keywords'); 3338 if (value !== null && value.trim() !== '') {continue;} 3339 dataElements[i].setAttribute("data-keywords", combined_tags); 3340 } 3341 } 3342}; 3343pushTags(); 3344 3345 3346const pushStreamTags = function() {
3347 let combined_tags = getKeywords(); 3348 if (combined_tags.length > 0 && combined_tags !== '') { 3349 let i, dataElements; 3350 dataElements = document.getElementsByClassName("eas6a97888e37"); 3351 for (i = 0; i<dataElements.length; i++) { 3352 dataElements[i].setAttribute("data-keywords", combined_tags); 3353 } 3354 } 3355}; 3356 3357(function () { 3358 let combine_tags = getKeywords(); 3359 let element = $('.outszone_1'); 3360 if (element.length > 0 && window.innerWidth <= 480) { 3361 element.css('display', 'none'); 3362 let agegoAttrs = ''; 3363 if (document.body.classList.contains('agego-unverified')) agegoAttrs = ' data-ex_av="2" data-block-ad-types="101"'; 3364 else if (document.body.classList.contains('agego-verified') || document.body.classList.contains('agego-legacy')) agegoAttrs = ' data-ex_av="1"'; 3365 element.replaceWith(`<div class="out_video container"><ins class="eas6a97888e37" data-zoneid="${varValues.adIds.outstream}" data-keywords="${combine_tags}"${agegoAttrs}></ins></div>`); 3366 pushStreamTags(); 3367 } else { 3368 element.find('ins').attr('class', 'eas6a97888e20'); 3369 element.find('ins').attr('data-keywords', combine_tags); 3370 } 3371 (AdProvider = window.AdProvider || []).push({"serve": {}}); 3372})(); 3373 3374 3375// NEW HOME SCRIPTS 3376// NEW TAGS ROW SCRIPT 3377const tags_CONTAINER = document.getElementById('tagsContainer'); 3378const tags_WRAPPER = document.querySelector('.tags-row-wrapper'); 3379const tags_SECTION = document.querySelector('.tags-row-section'); 3380const left_ARROW = document.getElementById('leftArrow'); 3381const right_ARROW = document.getElementById('rightArrow'); 3382let scroll_POSITION = 0; 3383const scroll_AMOUNT = 200; 3384 3385if(tags_CONTAINER){ 3386 function update_ARROWS() { 3387 const max_SCROLL = tags_CONTAINER.scrollWidth - tags_WRAPPER.clientWidth; 3388 3389 left_ARROW.disabled = scroll_POSITION <= 0; 3390 right_ARROW.disabled = scroll_POSITION >= max_SCROLL; 3391 3392 if (scroll_POSITION <= 0) { 3393 tags_SECTION.classList.add('hide-left'); 3394 } else { 3395 tags_SECTION.classList.remove('hide-left'); 3396 } 3397 3398 if (scroll_POSITION >= max_SCROLL) { 3399 tags_SECTION.classList.add('hide-right'); 3400 } else { 3401 tags_SECTION.classList.remove('hide-right'); 3402 } 3403 } 3404 3405 left_ARROW.addEventListener('click', () => { 3406 scroll_POSITION -= scroll_AMOUNT; 3407 if (scroll_POSITION < 0) scroll_POSITION = 0; 3408 tags_CONTAINER.style.transform = `translateX(-${scroll_POSITION}px)`; 3409 update_ARROWS(); 3410 }); 3411 3412 right_ARROW.addEventListener('click', () => { 3413 const max_SCROLL = tags_CONTAINER.scrollWidth - tags_WRAPPER.clientWidth; 3414 scroll_POSITION += scroll_AMOUNT; 3415 if (scroll_POSITION > max_SCROLL) scroll_POSITION = max_SCROLL; 3416 tags_CONTAINER.style.transform = `translateX(-${scroll_POSITION}px)`; 3417 update_ARROWS(); 3418 }); 3419 3420 window.addEventListener('resize', () => { 3421 scroll_POSITION = 0; 3422 tags_CONTAINER.style.transform = `translateX(0px)`; 3423 update_ARROWS(); 3424 }); 3425 3426 window.addEventListener('load', update_ARROWS); 3427} 3428 3429// NEW VIDEOS SCRIPTS 3430function getPreviewVideo(video_html_link, sparksThumbUrl) { 3431 const link_break = video_html_link.split('/'); 3432 const base_url_map = { 3433 brazzers: 'https://cdn3.aznude.com/data/preview/', 3434 realitykings: 'https://cdn4.aznude.com/preview/rk_', 3435 men: 'https://cdn5.aznude.com/preview/men_', 3436 default: sparksThumbUrl || varValues.sparksThumbUrl 3437 }; 3438 let final_link; 3439 if (video_html_link.includes('/data/brazzers/videos')) { 3440 final_link = base_url_map.brazzers + 3441 link_break[link_break.length - 2] + '/' + 3442 link_break[link_break.length - 1].replace('.html', '_sum_mute.mp4'); 3443 } else if (video_html_link.includes('/data/realitykings/videos')) { 3444 final_link = base_url_map.realitykings + 3445 link_break[link_break.length - 2] + '/' + 3446 link_break[link_break.length - 1].replace('.html', '_sum_mute.mp4'); 3447 } else if (video_html_link.includes('/data/men/videos')) { 3448 final_link = base_url_map.men + 3449 link_break[link_break.length - 2] + '/' + 3450 link_break[link_break.length - 1].replace('.html', '_sum_mute.mp4'); 3451 } else { 3452 // The favourites page lists other sites' items too, so the base comes 3453 // from the item's own host; the page's sparksThumbUrl is only the 3454 // fallback for relative links and unknown hosts. Matched on the host or 3455 // a subdomain of it, never a substring: 'aznude.com' must not match 3456 // aznudefeet.com or aznude.com.evil.example. 3457 const sparksBaseByHost = { 3458 'aznude.com': 'https://cdn2.aznude.com/sparkthumbs/', 3459 'azmen.com': 'https://cdn-men.aznude.com/sparkthumbs/', 3460 'azfans.com': 'https://cdn2.azfans.com/sparkthumbs/', 3461 'aznudefeet.com': 'https://cdn.aznudefeet.com/sparkthumbs/' 3462 }; 3463 let host = ''; 3464 try { 3465 host = new URL(video_html_link, location.href).hostname; 3466 } catch (e) { /* unparseable link: keep the page default */ } 3467 const site = Object.keys(sparksBaseByHost) 3468 .find(domain => host === domain || host.endsWith('.' + domain));
3469 const defaultBase = site ? sparksBaseByHost[site] : base_url_map.default; 3470 final_link = defaultBase + 3471 link_break[link_break.length - 1].replace('.html', '.mp4'); 3472 } 3473 return final_link; 3474} 3475 3476function setupVideoPreviews(enablePreviews, sparksThumbUrl) { 3477 // const items = document.querySelectorAll('.video-list-item:not(.ad)'); 3478 3479 // items.forEach(item => { 3480 // const newItem = item.cloneNode(true); 3481 // item.parentNode.replaceChild(newItem, item); 3482 // }); 3483 3484 document.querySelectorAll('.video-list-item:not(.ad):not(.playlist-js)').forEach(item => { 3485 let video = null; 3486 let isTouchActive = false; 3487 let hoverTimeout = null; 3488 let stillHovering = false; 3489 let waitForReset = false; 3490 3491 const link = item.querySelector('a'); 3492 // The React pages render a skeleton placeholder carrying this class 3493 // before they hydrate, and it holds no link. Without this guard the 3494 // addEventListener calls below throw, the throw escapes the forEach, 3495 // and every later item plus the .playlist-js loop below is skipped. 3496 if (!link) return; 3497 3498 function createVideo() { 3499 if (!enablePreviews || item.classList.contains('media-list-item--full')) return; 3500 if (item.querySelector('video')) return; 3501 3502 const videoSrc = getPreviewVideo(link.href, sparksThumbUrl); 3503 if (!videoSrc) return; 3504 3505 video = document.createElement('video'); 3506 video.muted = true; 3507 video.loop = true; 3508 video.preload = 'none'; 3509 video.playsInline = true; 3510 3511 const source = document.createElement('source'); 3512 source.src = videoSrc; 3513 source.type = source.src.endsWith('.webm') ? 'video/webm' : 'video/mp4'; 3514 video.appendChild(source); 3515 link.appendChild(video); 3516 3517 video.load(); 3518 3519 video.onloadeddata = (e) => { 3520 if (!stillHovering) { 3521 e.target.remove(); 3522 return; 3523 } 3524 3525 item.classList.remove('loading'); 3526 item.classList.add('active'); 3527 3528 e.target.play().then(() => item.classList.add('loaded')) 3529 .catch(() => { 3530 item.classList.remove('loaded', 'active', 'hiding-labels'); 3531 e.target.remove(); 3532 }); 3533 }; 3534 3535 video.onerror = () => { 3536 item.classList.remove('loading', 'active', 'hiding-labels'); 3537 if (video) { video.remove(); video = null; } 3538 }; 3539 } 3540 3541 function clearPreview() { 3542 stillHovering = false; 3543 item.classList.remove('hiding-labels'); 3544 3545 if (hoverTimeout !== null) { 3546 clearTimeout(hoverTimeout); 3547 hoverTimeout = null; 3548 } 3549 3550 if (video) { 3551 video.onloadeddata = null; 3552 video.onerror = null; 3553 video.pause(); 3554 video.remove(); 3555 video = null; 3556 } 3557 item.classList.remove('loading', 'loaded', 'active'); 3558 } 3559 3560 link.addEventListener('click', (e) => { 3561 clearPreview(); 3562 waitForReset = true; 3563 }); 3564 3565 link.addEventListener('mouseenter', () => { 3566 if (item.classList.contains('no-hover')) return; 3567 if (waitForReset) return; 3568 stillHovering = true; 3569 item.classList.add('hiding-labels'); 3570 if (!video && !isTouchActive && enablePreviews && !item.classList.contains('media-list-item--full')) { 3571 item.classList.add('loading'); 3572 hoverTimeout = setTimeout(createVideo, 1); 3573 } 3574 }); 3575 3576 link.addEventListener('mouseleave', () => { 3577 if (item.classList.contains('no-hover')) return; 3578 clearPreview(); 3579 waitForReset = false; 3580 }); 3581 3582 link.addEventListener('touchstart', () => { 3583 if (waitForReset) return; 3584 if (item.classList.contains('no-hover')) return; 3585 if (!video && !isTouchActive && enablePreviews && !item.classList.contains('media-list-item--full')) { 3586 isTouchActive = true; 3587 stillHovering = true; 3588 item.classList.add('loading', 'hiding-labels'); 3589 hoverTimeout = setTimeout(createVideo, 75); 3590 } 3591 }); 3592 3593 link.addEventListener('touchend', () => { 3594 if (item.classList.contains('no-hover')) return; 3595 clearPreview(); 3596 waitForReset = false;
3597 isTouchActive = false; 3598 }); 3599 }); 3600 3601 document.querySelectorAll('.video-list-item.playlist-js:not(.ad)').forEach(item => { 3602 let video = null; 3603 let isTouchActive = false; 3604 let hoverTimeout = null; 3605 let stillHovering = false; 3606 let waitForReset = false; 3607 let controls; 3608 3609 let parent_node = item.querySelector('a'); 3610 let links = item.querySelector("img[preview-src]").getAttribute('preview-src'); 3611 links = links.split(","); 3612 function createVideo() { 3613 let link; 3614 let preview_duration = 30000; 3615 let total_rotations = 10; 3616 let count = 0; 3617 function waitForVideoComplete(video, ms, callback) { 3618 let timeoutId; 3619 const handle = { 3620 timeoutId: null, cancel: () => { 3621 video.removeEventListener('ended', onComplete); 3622 clearTimeout(timeoutId); 3623 } 3624 }; 3625 const onComplete = () => { 3626 video.removeEventListener('ended', onComplete); 3627 clearTimeout(timeoutId); 3628 callback('ended', handle); 3629 }; 3630 video.addEventListener('ended', onComplete); 3631 timeoutId = setTimeout(() => { 3632 video.removeEventListener('ended', onComplete); 3633 callback('timeout: ' + timeoutId, handle); 3634 }, ms); 3635 handle.timeoutId = timeoutId; 3636 return handle; 3637 } 3638 3639 function get_next_video_element() { 3640 link = links[0]; 3641 links.push(links.shift()); 3642 if (!enablePreviews || item.classList.contains('media-list-item--full')) return null; 3643 const videoSrc = getPreviewVideo(link, sparksThumbUrl); 3644 if (!videoSrc) return; 3645 video = document.createElement('video'); 3646 video.muted = true; 3647 video.loop = false; 3648 video.preload = 'none'; 3649 video.playsInline = true; 3650 const source = document.createElement('source'); 3651 source.src = videoSrc; 3652 source.type = source.src.endsWith('.webm') ? 'video/webm' : 'video/mp4'; 3653 video.appendChild(source); 3654 video.load(); 3655 video.onloadeddata = (e) => { 3656 if (!stillHovering) { 3657 e.target.remove(); 3658 return; 3659 } 3660 item.classList.remove('loading'); 3661 item.classList.add('active'); 3662 e.target.play().then(() => item.classList.add('loaded')) 3663 .catch(() => { 3664 item.classList.remove('loaded', 'active', 'hiding-labels'); 3665 e.target.remove(); 3666 }); 3667 }; 3668 video.onerror = () => { 3669 item.classList.remove('loading', 'active', 'hiding-labels'); 3670 if (video) { 3671 video.remove(); 3672 video = null; 3673 } 3674 }; 3675 return video; 3676 } 3677 3678 function previewNextVideo() { 3679 video = get_next_video_element(); 3680 if (!video) return null; 3681 if (count === 0 && $(parent_node).find('video').length === 0) parent_node.appendChild(video); 3682 else $(parent_node).find('video').replaceWith(video); 3683 controls = waitForVideoComplete(video, preview_duration, 3684 (result, cancel) => { 3685 if (count < total_rotations && stillHovering) { 3686 cancel.cancel(); 3687 previewNextVideo(); 3688 } else { 3689 cancel.cancel(); 3690 } 3691 }); 3692 } 3693 previewNextVideo(); 3694 } 3695 3696 function clearPreview() { 3697 stillHovering = false;
3698 item.classList.remove('hiding-labels'); 3699 3700 if (hoverTimeout !== null) { 3701 clearTimeout(hoverTimeout); 3702 hoverTimeout = null; 3703 } 3704 if (controls) controls.cancel(); 3705 if (video) { 3706 video.onloadeddata = null; 3707 video.onerror = null; 3708 video.pause(); 3709 video.remove(); 3710 video = null; 3711 } 3712 item.classList.remove('loading', 'loaded', 'active'); 3713 } 3714 3715 parent_node.addEventListener('click', (e) => { 3716 clearPreview(); 3717 waitForReset = true; 3718 }) 3719 3720 parent_node.addEventListener('mouseenter', () => { 3721 if (item.classList.contains('no-hover')) return; 3722 if (waitForReset) return; 3723 stillHovering = true; 3724 item.classList.add('hiding-labels'); 3725 if (!video && !isTouchActive && enablePreviews && !item.classList.contains('media-list-item--full')) { 3726 item.classList.add('loading'); 3727 hoverTimeout = setTimeout(createVideo, 1); 3728 } 3729 }); 3730 3731 parent_node.addEventListener('mouseleave', () => { 3732 if (item.classList.contains('no-hover')) return; 3733 clearPreview(); 3734 waitForReset = false; 3735 }); 3736 3737 parent_node.addEventListener('touchstart', () => { 3738 if (waitForReset) return; 3739 if (item.classList.contains('no-hover')) return; 3740 if (!video && !isTouchActive && enablePreviews && !item.classList.contains('media-list-item--full')) { 3741 isTouchActive = true; 3742 stillHovering = true; 3743 item.classList.add('loading', 'hiding-labels'); 3744 hoverTimeout = setTimeout(createVideo, 75); 3745 } 3746 }); 3747 3748 parent_node.addEventListener('touchend', () => { 3749 if (item.classList.contains('no-hover')) return; 3750 clearPreview(); 3751 waitForReset = false; 3752 isTouchActive = false; 3753 }); 3754 }); 3755 3756} 3757 3758document.addEventListener('DOMContentLoaded', () => { 3759 setupVideoPreviews(varValues.shouldShowVideoPreviews); 3760 3761 const tabs = document.querySelectorAll('.videos-tab'); 3762 const tabPanes = document.querySelectorAll('.videos-tab-pane'); 3763 3764 if(tabs && tabPanes){
3765 tabs.forEach(tab => { 3766 tab.addEventListener('click', () => { 3767 tabs.forEach(t => t.classList.remove('active')); 3768 tabPanes.forEach(pane => pane.classList.remove('active')); 3769 3770 tab.classList.add('active'); 3771 const tabId = tab.getAttribute('data-tab'); 3772 document.getElementById(tabId).classList.add('active'); 3773 }); 3774 }); 3775 } 3776}); 3777 3778 3779function setupSlider(wrapper) { 3780 if (!wrapper || wrapper.__sliderInit) return; 3781 wrapper.__sliderInit = true; 3782 3783 var viewport = wrapper.querySelector('.media-slider-viewport'); 3784 var list = wrapper.querySelector('.media-list'); 3785 var leftBtn = wrapper.querySelector('.media-list--slider-btn-left'); 3786 var rightBtn = wrapper.querySelector('.media-list--slider-btn-right'); 3787 3788 function clamp(v, min, max) {return Math.max(min, Math.min(max, v));} 3789 3790 function getStep() { 3791 var first = list.querySelector('.media-list-item'); 3792 if (!first) return viewport.clientWidth * 0.9; 3793 var style = window.getComputedStyle(list); 3794 var gap = parseFloat(style.columnGap || style.gap || 10); 3795 return first.getBoundingClientRect().width + gap; 3796 } 3797 3798 function maxScrollLeft() {return Math.max(0, viewport.scrollWidth - viewport.clientWidth - 1);} 3799 3800 function updateArrows() { 3801 var maxScroll = maxScrollLeft(); 3802 leftBtn.disabled = viewport.scrollLeft <= 1; 3803 rightBtn.disabled = viewport.scrollLeft >= maxScroll; 3804 } 3805 3806 function scrollByStep(dir) { 3807 var next = viewport.scrollLeft + dir * getStep(); 3808 var maxLeft = maxScrollLeft(); 3809 viewport.scrollTo({ left: clamp(next, 0, maxLeft), behavior: 'smooth' }); 3810 } 3811 3812 function setButtonsTop() { 3813 var img = list.querySelector('.media-list-item img'); 3814 if (!img) return; 3815 var wrapRect = wrapper.getBoundingClientRect(); 3816 var imgRect = img.getBoundingClientRect(); 3817 var center = imgRect.top - wrapRect.top + imgRect.height / 2; 3818 wrapper.style.setProperty('--slider-btn-top', center + 'px'); 3819 } 3820 3821 leftBtn.addEventListener('click', function () {scrollByStep(-1);}); 3822 rightBtn.addEventListener('click', function () {scrollByStep(1);}); 3823 3824 viewport.addEventListener('keydown', function (e) { 3825 if (e.key === 'ArrowRight') {e.preventDefault(); scrollByStep(1);} 3826 if (e.key === 'ArrowLeft') {e.preventDefault(); scrollByStep(-1);} 3827 }); 3828 3829 var isDown = false, startX = 0, startLeft = 0, moved = false; 3830 viewport.addEventListener('mousedown', function (e) { 3831 isDown = true; 3832 moved = false; 3833 startX = e.clientX; 3834 startLeft = viewport.scrollLeft; 3835 viewport.classList.add('dragging'); 3836 }); 3837 3838 window.addEventListener('mouseup', function () { 3839 isDown = false; 3840 viewport.classList.remove('dragging'); 3841 }); 3842 3843 window.addEventListener('mousemove', function (e) { 3844 if (!isDown) return; 3845 var dx = e.clientX - startX; 3846 if (Math.abs(dx) > 3) moved = true; 3847 viewport.scrollLeft = startLeft - dx; 3848 }); 3849 3850 viewport.addEventListener('click', function (e) { 3851 if (moved) e.preventDefault(); 3852 }, true); 3853 3854 viewport.addEventListener('scroll', updateArrows, { passive: true }); 3855 window.addEventListener('resize', function () { 3856 updateArrows(); 3857 setButtonsTop(); 3858 }); 3859 3860 var imgs = list.querySelectorAll('img'); 3861 var pending = imgs.length; 3862 if (pending === 0) { 3863 requestAnimationFrame(function () { 3864 updateArrows(); 3865 setButtonsTop(); 3866 }); 3867 } else { 3868 imgs.forEach(function (img) { 3869 if (img.complete && img.naturalWidth) { 3870 if (--pending === 0) { 3871 updateArrows(); 3872 setButtonsTop(); 3873 } 3874 } else { 3875 img.addEventListener('load', function () { 3876 if (--pending === 0) { 3877 updateArrows(); 3878 setButtonsTop(); 3879 } 3880 }, { once: true }); 3881 img.addEventListener('error', function () { 3882 if (--pending === 0) { 3883 updateArrows(); 3884 setButtonsTop(); 3885 } 3886 }, { once: true }); 3887 } 3888 }); 3889 } 3890 3891 if ('ResizeObserver' in window) { 3892 var ro = new ResizeObserver(function () { 3893 updateArrows(); 3894 setButtonsTop(); 3895 }); 3896 ro.observe(list); 3897 ro.observe(viewport); 3898 } 3899 3900 requestAnimationFrame(function () { 3901 updateArrows(); 3902 setButtonsTop(); 3903 }); 3904 setTimeout(function () { 3905 updateArrows(); 3906 setButtonsTop(); 3907 }, 0); 3908} 3909 3910(function (){ 3911 document.addEventListener('DOMContentLoaded', function () { 3912 var wrappers = document.querySelectorAll('.media-slider-wrapper'); 3913 for (var i = 0; i < wrappers.length; i++) setupSlider(wrappers[i]); 3914 }); 3915})(); 3916 3917 3918 3919// NEW PAGINATION SCRIPTS 3920function resizePagination() { 3921 const paginations = document.querySelectorAll('.pagination-section'); 3922 if (!paginations.length) return; 3923 3924 paginations.forEach(pagination => { 3925 pagination.querySelectorAll('li').forEach(item => { 3926 item.style.display = 'inline-block'; 3927 }); 3928 3929 const activeItem = pagination.querySelector('.section-page-item.active'); 3930 const containerWidth = pagination.parentElement.parentElement.clientWidth; 3931 const items = Array.from(pagination.querySelectorAll('li')); 3932 const gap = 8; 3933 3934 let totalWidth = items.reduce((acc, curr) => { 3935 const style = window.getComputedStyle(curr); 3936 return acc + curr.clientWidth + parseFloat(style.marginLeft) + parseFloat(style.marginRight) + gap; 3937 }, 0); 3938 3939 if (totalWidth > containerWidth) { 3940 let availableSpace = containerWidth; 3941 const fixedButtons = pagination.querySelectorAll('li.section-next, li.section-last-page-item, li.section-pagination-rest'); 3942 const priorityButtons = [activeItem, activeItem?.previousElementSibling, activeItem?.nextElementSibling].filter(
3942Boolean); 3943 3944 const fixedAndPriorityWidth = [...fixedButtons, ...priorityButtons].reduce((acc, curr) => { 3945 if (curr) { 3946 const style = window.getComputedStyle(curr); 3947 acc += curr.clientWidth + parseFloat(style.marginLeft) + parseFloat(style.marginRight) + gap; 3948 } 3949 return acc; 3950 }, 0); 3951 3952 availableSpace -= fixedAndPriorityWidth; 3953 3954 const adjustableItems = Array.from(pagination.querySelectorAll('.section-page-item:not(.section-last-page-item, .active)')) 3955 .filter(item => !priorityButtons.includes(item)) 3956 .sort((a, b) => { 3957 const aPage = parseInt(a.querySelector('.section-page-link span').textContent); 3958 const bPage = parseInt(b.querySelector('.section-page-link span').textContent); 3959 return bPage - aPage; 3960 }); 3961 3962 for (const item of adjustableItems) { 3963 if (availableSpace <= 0) { 3964 item.style.display = 'none'; 3965 } else { 3966 const itemStyle = window.getComputedStyle(item); 3967 const itemFullWidth = item.clientWidth + parseFloat(itemStyle.marginLeft) + parseFloat(itemStyle.marginRight) + gap; 3968 if (availableSpace >= itemFullWidth) { 3969 availableSpace -= itemFullWidth; 3970 } else { 3971 item.style.display = 'none'; 3972 } 3973 } 3974 } 3975 } 3976 }); 3977} 3978 3979resizePagination(); 3980 3981window.addEventListener('resize', resizePagination); 3982 3983// NEW TAGS FOOTER SCRIPTS 3984const tagsFooterContainer = document.getElementById('tagsFooterContainer'); 3985const viewMoreContainer = document.getElementById('viewMoreContainer'); 3986const viewMoreBtn = document.getElementById('viewMoreBtn'); 3987 3988if(tagsFooterContainer){ 3989 let initialMaxHeight = window.innerWidth <= 600 ? 185 : 110; 3990 tagsFooterContainer.style.maxHeight = initialMaxHeight - 3 + 'px'; 3991 3992 function checkOverflow() { 3993 const hasOverflow = tagsFooterContainer.scrollHeight > tagsFooterContainer.clientHeight; 3994 if (hasOverflow) { 3995 viewMoreContainer.classList.add('visible'); 3996 } else { 3997 viewMoreContainer.classList.remove('visible'); 3998 tagsFooterContainer.classList.remove('expanded'); 3999 viewMoreBtn.textContent = 'More â¼'; 4000 } 4001 } 4002 4003 viewMoreBtn.addEventListener('click', () => { 4004 tagsFooterContainer.classList.toggle('expanded'); 4005 if (tagsFooterContainer.classList.contains('expanded')) { 4006 const fullHeight = tagsFooterContainer.scrollHeight; 4007 tagsFooterContainer.style.maxHeight = fullHeight + 'px'; 4008 viewMoreBtn.textContent = 'Less â²'; 4009 } else { 4010 tagsFooterContainer.style.maxHeight = initialMaxHeight - 3 + 'px'; 4011 viewMoreBtn.textContent = 'More â¼'; 4012 } 4013 }); 4014 4015 window.addEventListener('load', () => { 4016 checkOverflow(); 4017 }); 4018 4019 window.addEventListener('resize', () => { 4020 initialMaxHeight = window.innerWidth <= 600 ? 185 : 110; 4021 if (!tagsFooterContainer.classList.contains('expanded')) { 4022 tagsFooterContainer.style.maxHeight = initialMaxHeight - 3 + 'px'; 4023 } 4024 checkOverflow(); 4025 }); 4026} 4027 4028// NEW SINGLE VIDEO PAGE 4029const shareBtn = document.querySelector('.single-video-share-btn'); 4030const shareModal = document.getElementById('single-video-share-modal'); 4031const closeShareModal = document.querySelector('.single-video-close-modal'); 4032const favoriteBtn = document.querySelector('.single-video-favorite-btn'); 4033const toggleButton = document.querySelector('.toggle-button'); 4034const movieInfoContent = document.querySelector('.movie-info-content'); 4035 4036if (shareModal) { 4037 shareBtn.addEventListener('click', () => { 4038 shareModal.style.display = 'flex'; 4039 shareModal.showModal(); 4040 }); 4041 4042 closeShareModal.addEventListener('click', () => { 4043 shareModal.close(); 4044 shareModal.style.display = 'none'; 4045 }); 4046 4047 shareModal.addEventListener('click', (e) => { 4048 const rect = shareModal.querySelector('.single-video-modal-content').getBoundingClientRect(); 4049 const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.bottom && 4050 rect.left <= e.clientX && e.clientX <= rect.right); 4051 if (!isInDialog) { 4052 shareModal.close(); 4053 shareModal.style.display = 'none'; 4054 } 4055 }); 4056} 4057 4058if(favoriteBtn){ 4059 favoriteBtn.addEventListener('click', () => { 4060 favoriteBtn.classList.toggle('favorited'); 4061 }); 4062} 4063 4064if(toggleButton){ 4065 toggleButton.addEventListener('click', () => { 4066 movieInfoContent.classList.toggle('visible'); 4067 const span = toggleButton.querySelector('span'); 4068 span.textContent = movieInfoContent.classList.contains('visible') ?'Less' : 'More'; 4069 }); 4070} 4071 4072document.querySelectorAll('.single-video-input-group').forEach(group => { 4073 const input = group.querySelector('input'); 4074 4075 UniversalTooltip.attach(group, { 4076 trigger: 'click', 4077 placement: 'top', 4078 content: 'Copied!', 4079 autoHide: 1000, 4080 }); 4081 4082 const copyText = async () => { 4083 try { 4084 await navigator.clipboard.writeText(input.value); 4085 } catch (err) { 4086 console.error('Failed to copy:', err); 4087 } 4088 }; 4089 4090 group.addEventListener('click', copyText); 4091}); 4092 4093//region NEW FILTERS SECTION 4094document.addEventListener('DOMContentLoaded', function() { 4095 if (!document.getElementById('sort-button')) return; 4096 const browseFilterToggle = document.querySelector('.browse-filter-toggle'); 4097 const browseFilterColumn = document.querySelector('.browse-filter-column'); 4098 const browseFilterOverlay = document.querySelector('.browse-filter-overlay'); 4099 const defaultText = document.getElementById('sort-button').firstElementChild.innerHTML; 4100 if (browseFilterToggle) { 4101 browseFilterToggle.addEventListener('click', () => { 4102 browseFilterColumn.classList.toggle('active'); 4103 browseFilterOverlay.classList.toggle('active'); 4104 }); 4105 } 4106 if (browseFilterOverlay) { 4107 browseFilterOverlay.addEventListener('click', () => { 4108 if (browseFilterColumn) browseFilterColumn.classList.remove('active'); 4109 browseFilterOverlay.classList.remove('active'); 4110 }); 4111 } 4112 4113 // Filter section interactions 4114 document.querySelectorAll('.browse-filter-section label:not(.tag-label)').forEach(label => { 4115 label.addEventListener('click', () => { 4116 label.classList.toggle('active'); 4117 }); 4118 }); 4119 document.querySelectorAll('.tag-label').forEach(label => { 4120 label.addEventListener('click', (e) => { 4121 if (e.target.classList.contains('hide-button') || e.target.parentElement.classList.contains('hide-button')) { 4122 return; 4123 } 4124 if (e.target.classList.contains('plus-symbol')) { 4125 return; 4126 } 4127 if (e.target.classList.contains('browse-filter-text')) { 4128 label.classList.toggle('active'); 4129 } 4130 }); 4131 }); 4132 4133 4134 //region Tooltip functionality */ 4135 // UniversalTooltip.attach('.slider-thumb', 4136 // {trigger: 'hover', placement: 'top', content: 'Drag to set filter'}); 4137 // UniversalTooltip.attach('.browse-filter-column .plus-symbol', 4138 // {trigger: 'hover', placement: 'top', content: 'Combine tag'}); 4139 // UniversalTooltip.attach('.browse-filter-column .hide-button span', 4140 // {trigger: 'hover', placement: 'top', content: 'Hide tag'}); 4141 //endregion */ 4142 4143 //region Slider */ 4144 const thumbMin = document.getElementById('thumb-min'); 4145 const thumbMax = document.getElementById('thumb-max'); 4146 const sliderRange = document.querySelector('.slider-range'); 4147 const sliderTrack = document.querySelector('.slider-track'); 4148 const positions = [0, 25, 50, 75, 100]; 4149 let minIndex = 0; 4150 let maxIndex = 4; 4151 4152 function updateSliderRange() { 4153 thumbMin.style.left = `${positions[minIndex]}%`; 4154 thumbMax.style.left = `${positions[maxIndex]}%`; 4155 sliderRange.style.left = `${positions[minIndex]}%`; 4156 sliderRange.style.width = `${positions[maxIndex] - positions[minIndex]}%`; 4157 } 4158 4159 function dragThumb(thumb, isMin) { 4160 let isDragging = false;
4161 4162 thumb.addEventListener('mousedown', (e) => { 4163 e.preventDefault(); 4164 isDragging = true; 4165 document.addEventListener('mousemove', onMouseMove); 4166 document.addEventListener('mouseup', onMouseUp); 4167 }); 4168 4169 function onMouseMove(e) { 4170 if (!isDragging) return; 4171 4172 const sliderRect = sliderTrack.getBoundingClientRect(); 4173 const trackWidth = sliderRect.width; 4174 const relativeX = e.clientX - sliderRect.left; 4175 const percentage = (relativeX / trackWidth) * 100; 4176 4177 let closestIndex = 0; 4178 let minDiff = Infinity; 4179 positions.forEach((pos, index) => { 4180 const diff = Math.abs(pos - percentage); 4181 if (diff < minDiff) { 4182 minDiff = diff; 4183 closestIndex = index; 4184 } 4185 }); 4186 4187 if (isMin) { 4188 if (closestIndex >= maxIndex) { 4189 closestIndex = maxIndex - 1; 4190 } 4191 minIndex = closestIndex; 4192 } else { 4193 if (closestIndex <= minIndex) { 4194 closestIndex = minIndex + 1; 4195 } 4196 maxIndex = closestIndex; 4197 } 4198 4199 updateSliderRange(); 4200 } 4201 4202 function onMouseUp() { 4203 isDragging = false; 4204 document.removeEventListener('mousemove', onMouseMove); 4205 document.removeEventListener('mouseup', onMouseUp); 4206 } 4207 4208 thumb.addEventListener('touchstart', (e) => { 4209 e.preventDefault(); 4210 isDragging = true; 4211 document.addEventListener('touchmove', onTouchMove); 4212 document.addEventListener('touchend', onTouchEnd); 4213 }); 4214 4215 function onTouchMove(e) { 4216 if (!isDragging) return; 4217 4218 const sliderRect = sliderTrack.getBoundingClientRect(); 4219 const trackWidth = sliderRect.width; 4220 const relativeX = e.touches[0].clientX - sliderRect.left; 4221 const percentage = (relativeX / trackWidth) * 100; 4222 4223 let closestIndex = 0; 4224 let minDiff = Infinity; 4225 positions.forEach((pos, index) => { 4226 const diff = Math.abs(pos - percentage); 4227 if (diff < minDiff) { 4228 minDiff = diff; 4229 closestIndex = index; 4230 } 4231 }); 4232 4233 if (isMin) { 4234 if (closestIndex >= maxIndex) { 4235 closestIndex = maxIndex - 1; 4236 } 4237 minIndex = closestIndex; 4238 } else { 4239 if (closestIndex <= minIndex) { 4240 closestIndex = minIndex + 1; 4241 } 4242 maxIndex = closestIndex; 4243 } 4244 4245 updateSliderRange(); 4246 } 4247 4248 function onTouchEnd() { 4249 isDragging = false; 4250 document.removeEventListener('touchmove', onTouchMove); 4251 document.removeEventListener('touchend', onTouchEnd); 4252 } 4253 } 4254 4255 if(sliderTrack){ 4256 sliderTrack.addEventListener('click', (e) => { 4257 const sliderRect = sliderTrack.getBoundingClientRect(); 4258 const trackWidth = sliderRect.width; 4259 const relativeX = e.clientX - sliderRect.left; 4260 const percentage = (relativeX / trackWidth) * 100; 4261 4262 let closestIndex = 0; 4263 let minDiff = Infinity; 4264 positions.forEach((pos, index) => { 4265 const diff = Math.abs(pos - percentage); 4266 if (diff < minDiff) { 4267 minDiff = diff; 4268 closestIndex = index; 4269 } 4270 }); 4271 4272 const minDistance = Math.abs(positions[minIndex] - percentage); 4273 const maxDistance = Math.abs(positions[maxIndex] - percentage); 4274 4275 if (minDistance <= maxDistance) { 4276 if (closestIndex >= maxIndex) { 4277 minIndex = maxIndex - 1; 4278 } else { 4279 minIndex = closestIndex; 4280 } 4281 } else { 4282 if (closestIndex <= minIndex) { 4283 maxIndex = minIndex + 1; 4284 } else { 4285 maxIndex = closestIndex; 4286 } 4287 } 4288 4289 updateSliderRange(); 4290 }); 4291 4292 dragThumb(thumbMin, true); 4293 dragThumb(thumbMax, false); 4294 4295 updateSliderRange(); 4296 4297 window.addEventListener('resize', updateSliderRange); 4298 } 4299 //endregion ^^^^ Slider ^^^^ */ 4300 4301 4302 //region Dropdown Menu */ 4303 document.querySelectorAll('.sort-dropdown-wrapper').forEach(e => { 4304 let menu_button = e.querySelector('.sort-button'); 4305 menu_button.addEventListener("click", (e) => { 4306 if (!menu_button.parentElement.querySelector('.sort-dropdown').classList.contains('sort-button-off')) { 4307 close_dropdown_menus(); 4308 } else { 4309 close_dropdown_menus(); 4310 menu_button.parentElement.querySelector('.sort-dropdown').classList.toggle('sort-button-off'); 4311 } 4312 }); 4313 4314 e.querySelectorAll('.browse-filter-item').forEach(i => { 4315 i.addEventListener('click', (c) => { 4316 if (i.classList.contains('selected')){ 4317 c.preventDefault(); 4318 return; 4319 } 4320 if (i.classList.contains('submenu')){ 4321 c.preventDefault(); 4322 } 4323 if (e.id === 'sort_menu') { 4324 hide_other_menus(e); 4325 } 4326 reset_menu_selection(e); 4327 if (!i.classList.contains('selected')) { 4328 i.classList.add('selected'); 4329 } 4330 e.querySelector('.sort-button').firstElementChild.innerHTML = c.target.innerText; 4331 4332 if (i.hasAttribute('submenu')) { 4333 document.getElementById(i.getAttribute('submenu')).classList.toggle('menu-active'); 4334 } 4335 close_dropdown_menus(); 4336 }); 4337 }); 4338 }) 4339 4340 function close_dropdown_menus() { 4341 document.querySelectorAll('.sort-dropdown').forEach(e => { 4342 e.classList.add('sort-button-off'); 4343 }) 4344 } 4345 4346 function hide_other_menus(current_menu) { 4347 document.querySelectorAll('.sort-dropdown-wrapper').forEach(e => { 4348 if (e.id !== current_menu.id && e.id !== 'sort_menu') e.classList.remove('menu-active'); 4349 }); 4350 } 4351 4352 function reset_menu_selection(parentElement) { 4353 Array.from(parentElement.querySelectorAll('.browse-filter-item')).forEac
4353h(child => { 4354 child.classList.remove('selected'); 4355 }); 4356 } 4357 4358 document.addEventListener("click", function(e) { 4359 const sortButton = e.target.closest('.sort-button'); 4360 const selectedItem = e.target.closest('.browse-filter-item'); 4361 if (sortButton || selectedItem) { 4362 return; 4363 } 4364 close_dropdown_menus(); 4365 }); 4366 4367 //endregion ^^^^ Dropdown Menu ^^^^ */ 4368}); 4369//endregion 4370 4371(function () { 4372 const load = src => new Promise((res, rej) => { 4373 const s = document.createElement('script'); 4374 s.src = src; s.async = false; s.defer = true; 4375 s.onload = res; s.onerror = rej; document.head.appendChild(s); 4376 }); 4377 const ready = (pred, t=5000, step=50) => new Promise((res, rej) => { 4378 const end = Date.now()+t; (function tick(){ 4379 try { if (pred()) return res(); } catch {} 4380 return Date.now()>end ? rej(new Error('timeout')) : setTimeout(tick, step); 4381 })(); 4382 }); 4383 4384 window.AdProvider = window.AdProvider || []; 4385 4386 (async () => { 4387 await load('https://a.magsrv.com/ad-provider.js'); 4388 await ready(() => typeof window.AdProvider.push === 'function'); 4389 await load('https://a.pemsrv.com/ad-provider.js'); 4390 window.AdProvider.push({ serve: {} }); 4391 })(); 4392})(); 4393 4394// Popup Ads 4395const isChromium=!!window.chrome; 4396const isMobile=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 4397 4398(function(){ 4399 const nativeID = isMobile ? varValues.popupAd.native_id_mobile : varValues.popupAd.native_id_desktop; 4400 const ins = document.createElement("ins"); 4401 let combined_tags = getKeywords(); 4402 // ExoClick's current "Get code" for the native zones uses eas6a97888e20. 4403 // eas6a97888e was the older tag. The interstitial below keeps its own class 4404 // (eas6a97888e33) and the outstream keeps eas6a97888e37 - different zones. 4405 ins.className="eas6a97888e20"; 4406 ins.setAttribute("data-zoneid",nativeID); 4407 ins.setAttribute("data-keywords", combined_tags); 4408 if(document.body.classList.contains('agego-unverified')) { 4409 ins.setAttribute("data-ex_av", "2"); 4410 ins.setAttribute("data-block-ad-types", "101"); 4411 } else if(document.body.classList.contains('agego-verified') || document.body.classList.contains('agego-legacy')) { 4412 ins.setAttribute("data-ex_av", "1"); 4413 } 4414 if(document.getElementById(varValues.popupAd.native_tag)){ 4415 document.getElementById(varValues.popupAd.native_tag).appendChild(ins); 4416 } 4417 4418 const root = document.getElementById('react-root'); 4419 const noadpages = ['search']; 4420 if (root && noadpages.includes(root.dataset.page)) return; 4421 4422 if(isChromium){ 4423 const interId = isMobile ? varValues.popupAd.inter_id_mobile : varValues.popupAd.inter_id_desktop; 4424 const ins = document.createElement("ins"); 4425 ins.className = "eas6a97888e33"; 4426 ins.setAttribute("data-zoneid", interId); 4427 ins.setAttribute("data-keywords", combined_tags); 4428 if(document.body.classList.contains('agego-unverified')) { 4429 ins.setAttribute("data-ex_av", "2"); 4430 ins.setAttribute("data-block-ad-types", "101"); 4431 } else if(document.body.classList.contains('agego-verified') || document.body.classList.contains('agego-legacy')) { 4432 ins.setAttribute("data-ex_av", "1"); 4433 } 4434 return document.body.appendChild(ins); 4435 } 4436 4437 // let customTargeting = {}; 4438 // if(document.body.classList.contains('agego-unverified')) { 4439 // customTargeting = { ex_av: '2', block_ad_types: '101' }; 4440 // } else if(document.body.classList.contains('agego-verified') || document.body.classList.contains('agego-legacy')) { 4441 // customTargeting = { ex_av: '1' }; 4442 // } 4443 4444 const adConfig = { 4445 "ads_host": "a.pemsrv.com", 4446 "syndication_host": "s.pemsrv.com", 4447 "idzone": varValues.popupAd.pops_id, 4448 "popup_fallback": false, 4449 "popup_force": false, 4450 "chrome_enabled": false, 4451 "new_tab": false, 4452 "frequency_period": 60, 4453 "frequency_count": 1, 4454 "trigger_method": 4, 4455 "trigger_class": "noexopop", 4456 "trigger_delay": 0, 4457 "capping_enabled": true, 4458 "tcf_enabled": true, 4459 "only_inline": false,
4460 "tags": combined_tags, 4461 // "customTargeting": customTargeting 4462 }; 4463 4464 window.document.querySelectorAll||(document.querySelectorAll=document.body.querySelectorAll=Object.querySelectorAll=function(e,o,t,i,n){var r=document,a=r.createStyleSheet();for(n=r.all,o=[],t=(e=e.replace(/\[for\b/gi,"[htmlFor").split(",")).length;t--;){for(a.addRule(e[t],"k:v"),i=n.length;i--;)n[i].currentStyle.k&&o.push(n[i]);a.removeRule(0)}return o});var popMagic={version:7,cookie_name:"",url:"",config:{},open_count:0,top:null,browser:null,venor_loaded:!1,venor:!1,tcfData:null,configTpl:{ads_host:"",syndication_host:"",idzone:"",frequency_period:720,frequency_count:1,trigger_method:1,trigger_class:"",popup_force:!1,popup_fallback:!1,chrome_enabled:!0,new_tab:!1,cat:"",tags:"",el:"",sub:"",sub2:"",sub3:"",only_inline:!1,trigger_delay:0,capping_enabled:!0,tcf_enabled:!1,cookieconsent:!0,should_fire:function(){return!0},on_redirect:null},init:function(e){if(void 0!==e.idzone&&e.idzone){void 0===e.customTargeting&&(e.customTargeting=[]),window.customTargeting=e.customTargeting||null;var o=Object.keys(e.customTargeting).filter(function(e){return e.search("ex_")>=0});for(var t in o.length&&o.forEach(function(e){return this.configTpl[e]=null}.bind(this)),this.configTpl)Object.prototype.hasOwnProperty.call(this.configTpl,t)&&(void 0!==e[t]?this.config[t]=e[t]:this.config[t]=this.configTpl[t]);if(void 0!==this.config.idzone&&""!==this.config.idzone){!0!==this.config.only_inline&&this.loadHosted();var i=this;this.checkTCFConsent(function(){"complete"===document.readyState?i.preparePopWait():i.addEventToElement(window,"load",i.preparePop)})}}},getCountFromCookie:function(){if(!this.config.cookieconsent)return 0;var e=popMagic.getCookie(popMagic.cookie_name),o=void 0===e?0:parseInt(e);return isNaN(o)&&(o=0),o},getLastOpenedTimeFromCookie:function(){var e=popMagic.getCookie(popMagic.cookie_name),o=null;if(void 0!==e){var t=e.split(";")[1];o=t>0?parseInt(t):0}return isNaN(o)&&(o=null),o},shouldShow:function(e){if(e=e||!1,!popMagic.config.capping_enabled){var o=!0,t=popMagic.config.should_fire;try{e||"function"!=typeof t||(o=Boolean(t()))}catch(e){console.error("Error executing should fire callback function:",e)}return o&&0===popMagic.open_count}if(popMagic.open_count>=popMagic.config.frequency_count)return!1;var i=popMagic.getCountFromCookie(),n=popMagic.getLastOpenedTimeFromCookie(),r=Math.floor(Date.now()/1e3),a=n+popMagic.config.trigger_delay;return!(n&&a>r)&&(popMagic.open_count=i,!(i>=popMagic.config.frequency_count))},venorShouldShow:function(){return popMagic.venor_loaded&&"0"===popMagic.venor},setAsOpened:function(e){var o=e?e.target||e.srcElement:null,t={id:"",tagName:"",classes:"",text:"",href:"",elm:""};void 0!==o&&null!=o&&(t={id:void 0!==o.id&&null!=o.id?o.id:"",tagName:void 0!==o.tagName&&null!=o.tagName?o.tagName:"",classes:void 0!==o.classList&&null!=o.classList?o.classList:"",text:void 0!==o.outerText&&null!=o.outerText?o.outerText:"",href:void 0!==o.href&&null!=o.href?o.href:"",elm:o});var i=new CustomEvent("creativeDisplayed-"+popMagic.config.idzone,{detail:t});if(document.dispatchEvent(i),popMagic.config.capping_enabled){var n=1;n=0!==popMagic.open_count?popMagic.open_count+1:popMagic.getCountFromCookie()+1;var r=Math.floor(Date.now()/1e3);popMagic.config.cookieconsent&&popMagic.setCookie(popMagic.cookie_name,n+";"+r,popMagic.config.frequency_period)}else++popMagic.open_count},loadHosted:function(){var e=document.createElement("script");for(var o in e.type="application/javascript",e.async=!0,e.src="//"+this.config.ads_host+"/popunder1000.js",e.id="popmagicldr",this.config)Object.prototype.hasOwnProperty.call(this.config,o)&&"ads_host"!==o&&"syndication_host"!==o&&e.setAttribute("data-exo-"+o,this.config[o]);var t=document.getElementsByTagName("body").item(0);t.firstChild?t.insertBefore(e,t.firstChild):t.appendChild(e)},preparePopWait:function(){setTimeout(popMagic.preparePop,400)},preparePop:function(){if("object"!=typeof exoJsPop101||!Object.prototype.hasOwnProperty.call(exoJsPop101,"add")){if(popMagic.top=self,popMagic.top!==self)try{top.document.location.toString()&&(popMagic.top=top)}catch(e){}if(popMagic.cookie_name="zone-cap-"+popMagic.config.idzone,popMagic.config.capping_enabled||(document.cookie=popMagic.cookie_name+"=;expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/"),popMagic.shouldShow(!0)){var e=new XMLHttpRequest;e.onreadystatechange=function(){e.readyState==XMLHttpRequest.DONE&&(popMagic.venor_loaded=!0,200==e.status?popMagic.venor=e.responseText:popMagic.venor="0")};var o="https:"!==document.location.protocol&&"http:"!==document.location.protocol?"https:":document.location.protocol;e.open("GET",o+"//"+popMagic.config.syndication_host+"/venor.php",!0);try{e.send()}catch(e){popMagic.venor_loaded=!0}}if(popMagic.buildUrl(),popMagic.browser=popMagic.browserDetector.getBrowserInfo(),popMagic.config.chrome_enabled||!popMagic.browser.isChrome){var t=popMagic.getPopMethod(popMagic.browser);popMagic.addEvent("click",t)}}},getPopMethod:function(e){return popMagic.config.popup_force||popMagic.config.popup_fallback&&e.isChrome&&e.version>=68&&!e.isMobile?popMagic.methods.popup:e.isMobile?popMagic.methods.default:e.isChrome?popMagic.methods.chromeTab:popMagic.methods.default},checkTCFConsent:function(e){if(this.config.tcf_enabled&&"function"==typeof window.__tcfapi){var o=this;window.__tcfapi("addEventListener",2,function(t,i){i&&(o.tcfData=t,"tcloaded"!==t.eventStatus&&"useractioncomplete"!==t.eventStatus||(window.__tcfapi("removeEventListener",2,function(){},t.listenerId),e()))})}else e()},buildUrl:function(){var e,o="https:"!==document.location.protocol&&"http:"!==document.location.protocol?"https:":document.location.protocol,t=top===self?document.URL:document.referrer,i={type:"inline",name:"popMagic",ver:this.version},n="";customTargeting&&Object.keys(customTargeting).length&&("object"==typeof customTargeting?Object.keys(customTargeting):customTargeting).forEach(function(o){"object"==typeof customTargeting?e=customTargeting[o]:Array.isArray(customTargeting)&&(e=scriptEl.getAttribute(o));var t=o.replace("data-exo-","");n+="&"+t+"="+e});var r=this.tcfData&&this.tcfData.gdprApplies&&!0===this.tcfData.gdprApplies?1:0;this.url=o+"//"+this.config.syndication_host+"/v1/link.php?cat="+this.config.cat+"&idzone="+this.config.idzone+"&type=8&p="+encodeURIComponent(t)+"&sub="+this.config.sub+(""!==this.config.sub2?"&sub2="+this.config.sub2:"")+(""!==this.config.sub3?"&sub3="+this.config.sub3:"")+"&block=1&el="+this.config.el+"&tags="+this.config.tags+"&scr_info="+function(e){var o=e.type+"|"+e.name+"|"+e.ver;return encodeURIComponent(btoa(o))}(i)+n+"&gdpr="+r+"&cb="+Math.floor(1e9*Math.random()),this.tcfData&&this.tcfData.tcString?this.url+="&gdpr_consent="+encodeURIComponent(this.tcfData.tcString):this.url+="&cookieconsent="+this.config.cookieconsent},addEventToElement:function(e,o,t){e.addEventListener?e.addEventListener(o,t,!1):e.attachEvent?(e["e"+o+t]=t,e[o+t]=function(){e["e"+o+t](window.event)},e.attachEvent("on"+o,e[o+t])):e["on"+o]=e["e"+o+t]},getTriggerClasses:function(){var e,o=[];-1===popMagic.config.trigger_class.indexOf(",")?e=popMagic.config.trigger_class.split(" "):e=popMagic.config.trigger_class.replace(/\s/g,"").split(",");for(var t=0;t<e.length;t++)""!==e[t]&&o.push("."+e[t]);return o},addEvent:function(e,o){var t;if("3"!=popMagic.config.trigger_method)if("2"!=popMagic.config.trigger_method||""==popMagic.config.trigger_method)if("4"!=popMagic.config.trigger_method||""==popMagic.config.trigger_method)popMagic.addEventToElement(document,e,o);
4464else{var n=popMagic.getTriggerClasses();popMagic.addEventToElement(document,e,function(e){n.some(function(o){return null!==e.target.closest(o)})||o.call(e.target,e)})}else{var r=popMagic.getTriggerClasses();for(t=document.querySelectorAll(r.join(", ")),i=0;i<t.length;i++)popMagic.addEventToElement(t[i],e,o)}else for(t=document.querySelectorAll("a"),i=0;i<t.length;i++)popMagic.addEventToElement(t[i],e,o)},setCookie:function(e,o,t){if(!this.config.cookieconsent)return!1;t=parseInt(t,10);var i=new Date;i.setMinutes(i.getMinutes()+parseInt(t));var n=encodeURIComponent(o)+"; expires="+i.toUTCString()+"; path=/";document.cookie=e+"="+n},getCookie:function(e){if(!this.config.cookieconsent)return!1;var o,t,i,n=document.cookie.split(";");for(o=0;o<n.length;o++)if(t=n[o].substr(0,n[o].indexOf("=")),i=n[o].substr(n[o].indexOf("=")+1),(t=t.replace(/^\s+|\s+$/g,""))===e)return decodeURIComponent(i)},randStr:function(e,o){for(var t="",i=o||"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",n=0;n<e;n++)t+=i.charAt(Math.floor(Math.random()*i.length));return t},isValidUserEvent:function(e){return!(!("isTrusted"in e)||!e.isTrusted||"ie"===popMagic.browser.name||"safari"===popMagic.browser.name)||0!=e.screenX&&0!=e.screenY},isValidHref:function(e){if(void 0===e||""==e)return!1;return!/\s?javascript\s?:/i.test(e)},findLinkToOpen:function(e){var o=e,t=!1;try{for(var i=0;i<20&&!o.getAttribute("href")&&o!==document&&"html"!==o.nodeName.toLowerCase();)o=o.parentNode,i++;var n=o.getAttribute("target");n&&-1!==n.indexOf("_blank")||(t=o.getAttribute("href"))}catch(e){}return popMagic.isValidHref(t)||(t=!1),t||window.location.href},getPuId:function(){return"ok_"+Math.floor(89999999*Math.random()+1e7)},executeOnRedirect:function(){try{popMagic.config.capping_enabled||"function"!=typeof popMagic.config.on_redirect||popMagic.config.on_redirect()}catch(e){console.error("Error executing on redirect callback:",e)}},browserDetector:{browserDefinitions:[["firefox",/Firefox\/([0-9.]+)(?:\s|$)/],["opera",/Opera\/([0-9.]+)(?:\s|$)/],["opera",/OPR\/([0-9.]+)(:?\s|$)$/],["edge",/Edg(?:e|)\/([0-9._]+)/],["ie",/Trident\/7\.0.*rv:([0-9.]+)\).*Gecko$/],["ie",/MSIE\s([0-9.]+);.*Trident\/[4-7].0/],["ie",/MSIE\s(7\.0)/],["safari",/Version\/([0-9._]+).*Safari/],["chrome",/(?!Chrom.*Edg(?:e|))Chrom(?:e|ium)\/([0-9.]+)(:?\s|$)/],["chrome",/(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9.]+)(:?\s|$)/],["bb10",/BB10;\sTouch.*Version\/([0-9.]+)/],["android",/Android\s([0-9.]+)/],["ios",/Version\/([0-9._]+).*Mobile.*Safari.*/],["yandexbrowser",/YaBrowser\/([0-9._]+)/],["crios",/CriOS\/([0-9.]+)(:?\s|$)/]],isChromeOrChromium:function(){var e=window.navigator,o=(e.userAgent||"").toLowerCase(),t=e.vendor||"";if(-1!==o.indexOf("crios"))return!0;if(e.userAgentData&&Array.isArray(e.userAgentData.brands)&&e.userAgentData.brands.length>0){var i=e.userAgentData.brands,n=i.some(function(e){return"Google Chrome"===e.brand}),r=i.some(function(e){return"Chromium"===e.brand})&&2===i.length;return n||r}var a=!!window.chrome,c=-1!==o.indexOf("edg"),p=!!window.opr||-1!==o.indexOf("opr"),s=!(!e.brave||!e.brave.isBrave),g=-1!==o.indexOf("vivaldi"),d=-1!==o.indexOf("yabrowser"),l=-1!==o.indexOf("samsungbrowser"),u=-1!==o.indexOf("ucbrowser");return a&&"Google Inc."===t&&!c&&!p&&!s&&!g&&!d&&!l&&!u},getBrowserInfo:function(){var e=window.navigator.userAgent,o={name:"other",version:"1.0",versionNumber:1,isChrome:this.isChromeOrChromium(),isMobile:!!e.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WebOS|W
4464indows Phone/i)};for(var t in this.browserDefinitions){var i=this.browserDefinitions[t];if(i[1].test(e)){var n=i[1].exec(e),r=n&&n[1].split(/[._]/).slice(0,3),a=Array.prototype.slice.call(r,1).join("")||"0";r&&r.length<3&&Array.prototype.push.apply(r,1===r.length?[0,0]:[0]),o.name=i[0],o.version=r.join("."),o.versionNumber=parseFloat(r[0]+"."+a);break}}return o}},methods:{default:function(e){if(!popMagic.shouldShow()||!popMagic.venorShouldShow()||!popMagic.isValidUserEvent(e))return!0;var o=e.target||e.srcElement,t=popMagic.findLinkToOpen(o);return window.open(t,"_blank"),popMagic.setAsOpened(e),popMagic.executeOnRedirect(),popMagic.top.document.location=popMagic.url,void 0!==e.preventDefault&&(e.preventDefault(),e.stopPropagation()),!0},chromeTab:function(e){if(!popMagic.shouldShow()||!popMagic.venorShouldShow()||!popMagic.isValidUserEvent(e))return!0;if(void 0===e.preventDefault)return!0;e.preventDefault(),e.stopPropagation();var o=top.window.document.createElement("a"),t=e.target||e.srcElement;o.href=popMagic.findLinkToOpen(t),document.getElementsByTagName("body")[0].appendChild(o);var i=new MouseEvent("click",{bubbles:!0,cancelable:!0,view:window,screenX:0,screenY:0,clientX:0,clientY:0,ctrlKey:!0,altKey:!1,shiftKey:!1,metaKey:!0,button:0});i.preventDefault=void 0,o.dispatchEvent(i),o.parentNode.removeChild(o),popMagic.executeOnRedirect(),window.open(popMagic.url,"_self"),popMagic.setAsOpened(e)},popup:function(e){if(!popMagic.shouldShow()||!popMagic.venorShouldShow()||!popMagic.isValidUserEvent(e))return!0;var o="";if(popMagic.config.popup_fallback&&!popMagic.config.popup_force){var t=Math.max(Math.round(.8*window.innerHeight),300);o="menubar=1,resizable=1,width="+Math.max(Math.round(.7*window.innerWidth),300)+",height="+t+",top="+(window.screenY+100)+",left="+(window.screenX+100)}var i=document.location.href,n=window.open(i,popMagic.getPuId(),o);popMagic.setAsOpened(e),setTimeout(function(){n.location.href=popMagic.url,popMagic.executeOnRedirect()},200),void 0!==e.preventDefault&&(e.preventDefault(),e.stopPropagation())}}}; popMagic.init(adConfig); 4465})(); 4466 4467// (function() { 4468// if (!isChromium) return; 4469 4470// const DELAY_MS = 90000; 4471// const MIN_CLICKS = 3; 4472// const MAX_CLICKS = 6; 4473 4474// let interval = null; 4475 4476// if (!localStorage.getItem('pop_click_threshold')) { 4477// localStorage.setItem('pop_click_threshold', Math.floor(Math.random() * (MAX_CLICKS - MIN_CLICKS + 1)) + MIN_CLICKS); 4478// } 4479 4480// function checkTimer() { 4481// const start = parseInt(localStorage.getItem('pop_timer_start')); 4482// if (Date.now() - start >= DELAY_MS) { 4483// localStorage.setItem('pop_is_ready', 'true'); 4484// clearInterval(interval); 4485// } 4486// } 4487 4488// if (localStorage.getItem('pop_timer_start') && !localStorage.getItem('pop_is_ready') && !localStorage.getItem('pop_has_fired')) { 4489// interval = setInterval(checkTimer, 1000); 4490// } 4491 4492// document.querySelectorAll('a').forEach(link => { 4493// link.addEventListener('click', (e) => { 4494// if (localStorage.getItem('pop_has_fired')) return; 4495 4496// if (localStorage.getItem('pop_is_ready')) { 4497// e.preventDefault(); 4498// localStorage.setItem('pop_has_fired', 'true'); 4499// localStorage.removeItem('pop_click_threshold'); 4500// localStorage.removeItem('pop_click_count'); 4501// localStorage.removeItem('pop_timer_start'); 4502// localStorage.removeItem('pop_is_ready'); 4503// window.open(e.currentTarget.href, '_blank'); 4504// window.location.assign(`https://s.pemsrv.com/v1/link.php?idzone=${varValues.popupAd.pops_id}`); 4505// if (interval) clearInterval(interval); 4506// return; 4507// } 4508 4509// if (!localStorage.getItem('pop_timer_start')) { 4510// const threshold = parseInt(localStorage.getItem('pop_click_threshold')); 4511// const clicks = parseInt(localStorage.getItem('pop_click_count') || '0') + 1; 4512// localStorage.setItem('pop_click_count', clicks); 4513 4514// if (clicks >= threshold) { 4515// localStorage.setItem('pop_timer_start', Date.now()); 4516// interval = setInterval(checkTimer, 1000); 4517// } 4518// } 4519// }); 4520// }); 4521// })(); 4522 4523(function () { 4524 var STORAGE_KEY = 'az_seen_by_episode_v1'; // bump _vN per future nudge, never reuse 4525 4526 function seen() { try { return localStorage.getItem(STORAGE_KEY) === '1'; } catch (e) { return false; } } 4527 function mark() { try { localStorage.setItem(STORAGE_KEY, '1'); } catch (e) {} } 4528 4529 function initByEpisodeBadge() { 4530 if (seen()) return; 4531 var tab = document.getElementById('picker-guide'); // the By Episode tab 4532 if (!tab) return; 4533 4534 var ut = new UniversalTooltip(tab, { 4535 trigger: 'manual', 4536 placement: 'top', 4537 tipClass: 'az-newfeat', 4538 content: 4539 '<span class="az-newfeat__pill">NEW</span>' + 4540 '<button type="button" class="az-newfeat__close" aria-label="Dismiss">×</button>' 4541 }); 4542 ut.show(); 4543 4544 // absolute-on-body coords freeze at show(); late images + ad <ins> reflow the page 4545 var reflow = function () { ut._position(); }; 4546 window.addEventListener('resize', reflow); 4547 window.addEventListener('load', reflow); 4548 var ro = window.ResizeObserver ? new ResizeObserver(reflow) : null; 4549 if (ro) ro.observe(document.body); 4550 4551 function dismiss() { 4552 mark(); 4553 window.removeEventListener('resize', reflow); 4554 window.removeEventListener('load', reflow); 4555 if (ro) ro.disconnect(); 4556 ut.hide(); 4557 } 4558 4559 var close = ut.tip && ut.tip.querySelector('.az-newfeat__close'); 4560 if (close) close.addEventListener('click', dismiss); 4561 4562 tab.addEventListener('click', function once() { // also silence once they use it 4563 tab.removeEventListener('click', once);
4564 dismiss(); 4565 }); 4566 } 4567 4568 if (document.readyState === 'loading') { 4569 document.addEventListener('DOMContentLoaded', initByEpisodeBadge); 4570 } else { 4571 initByEpisodeBadge(); 4572 } 4573 window.initByEpisodeBadge = initByEpisodeBadge; // re-call after client-side nav if celeb pages are SPA 4574})(); 4575 4576// Rating Widget 4577if(document.querySelector('.rw-ui-container') && false){ 4578 !function(e, t, n, i) { 4579 window.RW_Async_Init = function() { 4580 let e = localStorage.getItem("azn_id"), 4581 t = { 4582 huid: "389641", 4583 uid: "912edb0fba39c7c9400c77af16173cb0", 4584 source: "website", 4585 options: { 4586 mobile: { 4587 showTrigger: !1 4588 }, 4589 size: "medium", 4590 style: "crystal", 4591 isDummy: !1, 4592 showInfo: !1 4593 } 4594 }; 4595 e && (t.vid = e), RW.init(t), RW.render(function() { 4596 var e = RW.getRatings(), 4597 t = e[Object.keys(e)[0]], 4598 n = 0 < t.votes ? t.rate / t.votes : 0; 4599 n = Math.round(n), $("#rating-widget-average").text(n) 4600 }) 4601 }; 4602 var r, s = e.getElementsByTagName(n)[0], 4603 a = "rw-js", 4604 o = e.location, 4605 c = "Y" + t.getFullYear() + "M" + t.getMonth() + "D" + t.getDate(), 4606 g = o.protocol, 4607 d = o.search.indexOf("DBG=") > -1 ? "" : ".min"; 4608 e.getElementById(a) || ((r = e.createElement(n)).id = a, r.async = !0, r.type = "text/javascript", r.src = g + "//" + ("https:" == g ? "secure." + i + "js/" : "js." + i) + "external" + d + ".js?ck=" + c, s.parentNode.insertBefore(r, s)) 4609 }(document, new Date, "script", "azrating.com/"); 4610} 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635// Values 4636// value_d = how many years ago the movie was released 4637// value_r = likes 4638// value_v = views 4639// value_az = A-Z 4640let MediaGallery = null; 4641(() => { 4642 'use strict'; 4643 4644 /* âââââââââââââââââââââââ 0. Utility Functions ââââââââââââââââââââââââââ */ 4645 const querySelector = (selector, context = document) => context.querySelector(selector); 4646 const querySelectorAll = (selector, context = document) => [...context.querySelectorAll(selector)]; 4647 const parseToInteger = (value, defaultValue = 0) => { 4648 const parsed = parseInt(value, 10); 4649 return Number.isNaN(parsed) ? defaultValue : parsed; 4650 }; 4651 const getCookieValue = key => (document.cookie.match(`(^|;)\\s*${key}\\s*=\\s*([^;]+)`) || [])[2] || '';
4652 const setCookieValue = (key, value, maxAgeSeconds = 3600) => ( 4653 document.cookie = `${key}=${value}; SameSite=Strict; path=/; max-age=${maxAgeSeconds}` 4654 ); 4655 4656 /* âââââââââââââââââââââââ 1. Application State ââââââââââââââââââââââââââ */ 4657 const AppState = { 4658 advertisementUrl: '', 4659 jwPlayerInitialized: false, 4660 activeVideoPlayers: [], 4661 get videoPlayCount() { return parseToInteger(getCookieValue('videoPlayedNumber')); }, 4662 incrementVideoCount() { setCookieValue('videoPlayedNumber', this.videoPlayCount + 1); } 4663 }; 4664 const incrementVideoPlayCount = () => AppState.incrementVideoCount(); 4665 4666 /* âââââââââââââââââââââââ 2. Out-Stream Ad Management âââââââââââââââââââ */ 4667 const repositionOutStreamAd = () => { 4668 const $outStreamContainer = $('.out_video').detach(); 4669 if (!$outStreamContainer.length) return; 4670 const $visibleMediaItems = $('section .media-list-item').filter((_, element) => $(element).css('display') !== 'none'); 4671 $visibleMediaItems[9] && $visibleMediaItems.eq(9).append($outStreamContainer); 4672 }; 4673 4674 /* âââââââââââââââââââââââ 3. Media Filtering and Sorting ââââââââââââââââ */ 4675 const filterMediaByAge = maxAge => { 4676 querySelectorAll('section.single-page__movie-wrapper').forEach(section => { 4677 const ageValue = parseToInteger(section.getAttribute('value_d'), -1); 4678 section.style.display = maxAge === 0 || (ageValue !== -1 && ageValue <= maxAge) ? 'block' : 'none'; 4679 }); 4680 repositionOutStreamAd(); 4681 }; 4682 4683 const sortMediaSections = ({ sortKey, sortOrder, isNumeric }) => { 4684 const sortedSections = querySelectorAll('section.single-page__movie-wrapper').sort((sectionA, sectionB) => { 4685 const valueA = sectionA.getAttribute(sortKey), valueB = sectionB.getAttribute(sortKey); 4686 const comparison = isNumeric ? parseToInteger(valueA) - parseToInteger(valueB) : valueA.localeCompare(valueB); 4687 return sortOrder === 'desc' ? -comparison : comparison; 4688 }); 4689 sortedSections[0]?.parentElement.append(...sortedSections); 4690 repositionOutStreamAd(); 4691 }; 4692 4693 /* Delegated event handler for filter and sort inputs */ 4694 document.addEventListener('change', event => { 4695 const target = event.target; 4696 if (target.matches('input[name="time"]')) { 4697 filterMediaByAge(parseToInteger(target.value)); 4698 } 4699 if (target.matches('input[name="order"]')) { 4700 querySelector('#media-filter-label').textContent = querySelector(`label[for="${target.id}"]`).innerText.toUpperCase(); 4701 sortMediaSections({ 4702 sortKey: target.getAttribute('key'), 4703 sortOrder: target.value, 4704 isNumeric: target.getAttribute('key') !== 'value' 4705 }); 4706 } 4707 }); 4708 4709 /* âââââââââââââââââââââââ 4. JW Player Configuration ââââââââââââââââââââ */ 4710 /* eslint-disable */ 4711 function generatePlayerSettings(element) { 4712 let aboutlink = element.find('a').attr('href') || ''; 4713 let container = element.find('.player-container').find('[id^="player"]'); 4714 let image = container.attr('i') || ''; 4715 let image_base = image ? image.split('gigantic-4')[0] : ''; 4716 let vtt_file = container.attr('thumb') || ''; 4717 let abouttext = container.attr('title') || ''; 4718 let related = container.attr('j') ? {autoplayertime: 10, file: container.attr('j'), onclick: 'play', oncomplete: 'pause'} : {} 4719 4720 let sources = container.attr('h') ? [{'file': container.attr('h')}]: []; 4721 if (container.attr('w') && container.attr('w') != '') { 4722 sources = [...sources, {'file': image_base + 'lo.webm','label': "360p"}, 4723 {'file': image_base + 'hi.webm', 'label': "480p"}, 4724 {'file': image_base + 'hd.webm', 'label': "720p", "default": 'true'}] 4725 if (container.attr('n') && container.attr('n') != '3') {sources.pop(); sources[sources.length - 1]["default"] = 'true';} 4726 } 4727 sources = [...sources, {'file': image_base + 'lo.mp4','label': "LQ"}, 4728 {'file': image_base + 'hi.mp4', 'label': "HQ"}, 4729 {'file': image_base + 'hd.mp4', 'label': "HD", "default": 'true'}] 4730 if (container.attr('n') && container.attr('n') != '3') {sources.pop(); sources[sources.length - 1]["default"] = 'true';} 4731 4732 let tracks = [{'kind': 'thumbnails', 'file': vtt_file}, 4733 {"kind": "captions", "file": container.attr('c') ?? '', "label": "Automated Captions"}] 4734 let playlist = [{ 4735 pid: "h58u90987bP9bQ", description: abouttext, image: image, 4736 title: abouttext, sources: sources, tracks: tracks 4737 }], settings = { 4738 width: '100%', height: '100%', aspectratio: '16:9', 4739 nextUpDisplay: false, autostart: false, mute: false, 4740 repeat: false, stretching: "uniform", responsive: true, 4741 cast: {}, ga: {label: "mediaid"} 4742 }; 4743 settings['title'] = abouttext; 4744 settings['playlist'] = playlist; 4745 settings['image'] = image; 4746 settings['related'] = related; 4747 settings['mediaid'] = aboutlink.split('/').pop().replace('.html', ''); 4748 settings['abouttext'] = abouttext;
4749 settings['aboutlink'] = aboutlink; 4750 settings['autoPause'] = {'viewability': true}; 4751 settings['advertising'] = {'client': 'vast', "skipoffset": 5}; 4752 settings['playbackRateControls'] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2]; 4753 return settings; 4754 } 4755 4756 function updateJWPlayerConfig(playerInstance, _, settings, adUrl) { 4757 settings.playlist && playerInstance.load(settings.playlist); 4758 settings.title && playerInstance.setConfig({ title: settings.title }); 4759 settings.aspectratio && playerInstance.setConfig({ aspectratio: settings.aspectratio }); 4760 settings.abouttext && playerInstance.setConfig({ abouttext: settings.abouttext }); 4761 settings.aboutlink && playerInstance.setConfig({ aboutlink: settings.aboutlink }); 4762 typeof settings.autostart !== 'undefined' && playerInstance.setConfig({ autostart: settings.autostart }); 4763 settings.autoPause && playerInstance.setConfig({ autoPause: settings.autoPause }); 4764 typeof settings.mute !== 'undefined' && playerInstance.setConfig({ mute: settings.mute }); 4765 return configurePlayerEvents(playerInstance, settings, adUrl); 4766 } 4767 4768 const configurePlayerEvents = (playerInstance, settings, adUrl) => { 4769 playerInstance.off('play pause seek time playlistItem adViewableImpression'); 4770 playerInstance.once('play', (e) => { 4771 let playCount = parseToInteger(getCookieValue('videoPlayedNumber')); 4772 playCount || incrementVideoPlayCount(); 4773 playCount = parseToInteger(getCookieValue('videoPlayedNumber')); 4774 [1, 4, 7].includes(playCount) && ( 4775 playerInstance.pause(), 4776 playerInstance.playAd(adUrl), 4777 playerInstance.play() 4778 ); 4779 incrementVideoPlayCount(); 4780 const playerElement = playerInstance.getContainer(); 4781 if (!$(playerElement).find('#bg-video').length) { 4782 $(playerElement).prepend('<div id="bg-video"></div>'); 4783 } 4784 $(playerElement).find('#bg-video').css('background-image', `url(${settings.image})`); 4785 $(playerElement).find('#bg-video').css('display', 'none'); 4786 }); 4787 if (settings.aspectratio) { 4788 playerInstance.on('playlistItem', () => $(playerInstance.getContainer()).find('#bg-video').remove()); 4789 playerInstance.on('adSkip adComplete', () => playerInstance.play()); 4790 playerInstance.on('play', () => { 4791 playerInstance.getState() !== 'playing' && setTimeout(() => 4792 playerInstance.getState() !== 'playing' && playerInstance.play(), 250); 4793 }); 4794 } 4795 return playerInstance; 4796 }; 4797 4798 const initializePlayer = (config, playerObject, adUrl, activePlayers, playerId = '') => { 4799 const playerInstance = playerObject.setup(config); 4800 playerInstance.addButton( 4801 'https://cdn.aznude.com/player/download-v7.svg', 4802 'Download Video', 4803 () => { 4804 const { allSources } = playerInstance.getPlaylistItem(playerInstance.getPlaylistIndex()); 4805 window.location.href = allSources[allSources.length - 1].file.replace('.webm', '.mp4'); 4806 }, 4807 'download' 4808 ); 4809 playerInstance.on('play adPlay adResume', () => { 4810 return; 4811 $('#view').attr('data-view') !== 'compact' && ( 4812 activePlayers.forEach((player, index) => player !== playerId && activePlayers.splice(index, 1)), 4813 activePlayers.push(playerId), 4814 activePlayers.length > 1 && playerInstance.pause() 4815 ); 4816 }); 4817 return configurePlayerEvents(playerInstance, config, adUrl); 4818 }; 4819 /* eslint-enable */ 4820 4821 /* âââââââââââââââââââââââ 5. View Mode Toggle (Compact/Full) ââââââââââââ */ 4822 const ViewModeToggle = (() => { 4823 const $viewToggleButton = $('#view'); 4824 if (!$viewToggleButton.length) return { initialize() {}, activePlayers: [] };
4825 let $sharedPlayerContainer = $('<div id="jwplayer_one_container"><div id="jwplayer_one"></div></div>').appendTo('body').find('#jwplayer_one'); 4826 let activePlayerIds = []; 4827 let outStreamAdVisible = false; 4828 const intersectionObserver = new IntersectionObserver(entries => 4829 entries.forEach(entry => entry.isIntersecting && (outStreamAdVisible = true)), 4830 { threshold: 0.8 } 4831 ); 4832 const fullViewClass = 'media-list-item--full'; 4833 4834 function switchToCompactView() { 4835 $('.media-list.media-list-five-columns').addClass('media-list-double-mobile'); 4836 4837 if (!$('#jwplayer_one_container').find('#jwplayer_one').length) { 4838 activePlayerIds.length && ($sharedPlayerContainer = $('#' + activePlayerIds.shift()).parents('.player-position').children().first()); 4839 jwplayer($sharedPlayerContainer[0]).pause(); 4840 $sharedPlayerContainer.detach().appendTo('#jwplayer_one_container'); 4841 } 4842 $('.out_video').length && (outStreamAdVisible ? $('.out_video').remove() : $('.out_video').hide()); 4843 $viewToggleButton.attr('data-view', 'load'); 4844 activePlayerIds = []; 4845 $('.media-list-item > a > img').each(function() { 4846 const $image = $(this); 4847 const $mediaItem = $image.closest('.media-list-item'); 4848 if ($image.parents('a[data-type="pic"]').length) { 4849 $image.attr('src', $image.attr('prev-src')); 4850 } else { 4851 const $link = $mediaItem.find('a').first(); 4852 const $playerContainer = $mediaItem.children('.player-container'); 4853 4854 $image.attr('src', $image.attr('prev-src')).removeAttr('prev-src'); 4855 $mediaItem.removeClass('media-list-item--playing media-list-item--vertical').off('click'); 4856 $link.append($image).css({ display: '', pointerEvents: '' }); 4857 $playerContainer.hide(); 4858 const $playIcon = $mediaItem.find('.full-play-icon'); 4859 $playIcon.hide(); 4860 $image.css('opacity', '100'); 4861 } 4862 }); 4863 $('.single-page_content-container .single-page-title-wrapper, .single-page_content-container .media-list').removeClass('media-list-one-column max-w-1000'); 4864 $('section .media-list-item').removeClass(fullViewClass).each(function() { 4865 const previousClass = $(this).attr('prev-class'); 4866 previousClass && $(this).attr('class', previousClass); 4867 }); 4868 $viewToggleButton.attr('data-view', 'compact'); 4869 $('.single-page_content-container').attr('data-view', 'compact'); 4870 setupVideoPreviews(true); 4871 MediaGallery.initialize(); 4872 } 4873 4874 function switchToFullView() { 4875 $viewToggleButton.attr('data-view', 'load'); 4876 $('.media-list.media-list-five-columns').removeClass('media-list-double-mobile'); 4877 $('.single-page_content-container .single-page-title-wrapper, .single-page_content-container .media-list').addClass('media-list-one-column max-w-1000'); 4878 $('section .media-list-item').each(function() { 4879 $(this).is('[prev-class]') || $(this).attr('prev-class', $(this).attr('class')); 4880 }).addClass(fullViewClass); 4881 let itemCount = 0; 4882 const adPosition = $('.media-list-item > a > img').length > 10 ? 10 : $('.media-list-item > a > img').length > 7 ? 5 : -1; 4883 let shouldInsertAd = false; 4884 4885 if ($('#jwplayer_one_container').find('#jwplayer_one').length) { 4886 $sharedPlayerContainer = $('#jwplayer_one_container').find('#jwplayer_one'); 4887 } 4888 4889 $('.media-list-item').each(function() { 4890 itemCount++; 4891 const $mediaItem = $(this); 4892 const $image = $mediaItem.find('a > img'); 4893 4894 if (!$('.out_video').length && adPosition !== -1 && itemCount === adPosition && !outStreamAdVisible) { 4895 let combined_tags = getKeywords(); 4896 let agegoAttrs = ''; 4897 if (document.body.classList.contains('agego-unverified')) agegoAttrs = ' data-ex_av="2" data-block-ad-types="101"'; 4898 else if (document.body.classList.contains('agego-verified') || document.body.classList.contains('agego-legacy')) agegoAttrs = ' data-ex_av="1"'; 4899 $mediaItem.after(`<div class="out_video container"><ins class="eas6a97888e37" data-zoneid="${outszone}" data-keywords="${combined_tags}"${agegoAttrs}></ins></div>`); 4900 intersectionObserver.observe($('.out_video')[0]); 4901 shouldInsertAd = true; 4902 } else { 4903 $('.out_video').show(); 4904 } 4905 4906 if ($image.parents('a[data-type="pic"]').length) { 4907 $image.is('[prev-src]') || $image.attr('prev-src', $image.attr('src')); 4908 $image.attr('src', $image.parents('a').attr('href')); 4909 $mediaItem.css({ paddingLeft: 0, paddingRight: 0 }); 4910 } else { 4911 const $playerContainer = $mediaItem.children('.player-container'); 4912 const $playerPosition = $playerContainer.find('.player-position'); 4913 const $playerElement = $playerPosition.find('[id]'); 4914 const playerId = $playerElement.attr('id'); 4915 4916 $image.is('[prev-src]') || $image.attr('prev-src', $image.attr('src')); 4917 $image.attr('src', $playerElement.attr('i')); 4918 $mediaItem.is('[prev-class]') || $mediaItem.attr('prev-class', $mediaItem.attr('class')); 4919 $mediaItem.addClass('media-list-item--playing').toggleClass('media-list-item--vertical', !!$playerElement.attr('s')); 4920 4921 let $playIcon = $mediaItem.find('.full-play-icon'); 4922 $playIcon.show(); 4923 $playerContainer.css('position', 'absolute'); 4924 $playerContainer.css('top', '0'); 4925 $playerContainer.css('left', '0'); 4926 $playerContainer.css('width', '100%'); 4927 $playerContainer.css('height', '100%'); 4928 //$mediaItem.find('a').css('pointer-events', 'n
4928one'); 4929 4930 $mediaItem.off('click').on('click', event => { 4931 if ($(event.currentTarget).closest('.no-hover').length > 0 || $(event.currentTarget).hasClass('no-hover')) return; 4932 4933 event.preventDefault(); 4934 if (!activePlayerIds.includes(playerId)) { 4935 $playIcon.trigger('click'); 4936 } 4937 }); 4938 $playIcon.off('click').on('click', event => { 4939 if ($mediaItem.closest('.no-hover').length > 0 || $mediaItem.hasClass('no-hover')) return; 4940 4941 event.stopPropagation(); 4942 event.preventDefault(); 4943 if (!activePlayerIds.includes(playerId)) { 4944 event.stopPropagation(); 4945 activePlayerIds.length && (() => { 4946 const previousPlayerId = activePlayerIds.shift(); 4947 const $previousContainer = $('#' + previousPlayerId).parents('.player-container'); 4948 $previousContainer.hide(); 4949 $previousContainer.css('pointer-events', ''); 4950 $previousContainer.siblings('a').find('img').css('opacity', '100'); 4951 $previousContainer.siblings('a').find('.full-play-icon').show(); 4952 $previousContainer.siblings('a').find('.video-country').show(); 4953 $previousContainer.siblings('a').find('.video-timestamp').show(); 4954 $sharedPlayerContainer = $previousContainer.find('.player-position').children().first(); 4955 })(); 4956 if ($playerPosition.children().first().attr('id') !== $sharedPlayerContainer.attr('id')) { 4957 $sharedPlayerContainer.detach(); 4958 $playerPosition.prepend($sharedPlayerContainer[0]); 4959 } 4960 const playerConfig = { ...generatePlayerSettings($mediaItem), mute: false, autostart: true }; 4961 $playerElement.attr('s') && (playerConfig.aspectratio = '9:16'); 4962 4963 if(AppState.jwPlayerInitialized) { 4964 updateJWPlayerConfig(jwplayer($sharedPlayerContainer[0]), $sharedPlayerContainer[0], playerConfig, AppState.advertisementUrl); 4965 } else { 4966 initializePlayer(playerConfig, jwplayer($sharedPlayerContainer[0]), AppState.advertisementUrl, AppState.activeVideoPlayers, playerId); 4967 AppState.jwPlayerInitialized = true 4968 } 4969 setTimeout(() => { 4970 const playerInstance = jwplayer($sharedPlayerContainer[0]); 4971 playerInstance.seek(0).play().setVolume(25); 4972 }, 1) 4973 $playerContainer.show(); 4974 $playIcon.hide(); 4975 $mediaItem.find('.video-country').hide(); 4976 $mediaItem.find('.video-timestamp').hide(); 4977 $image.css('opacity', '0'); 4978 activePlayerIds.push(playerId); 4979 } 4980 }); 4981 } 4982 }); 4983 $viewToggleButton.attr('data-view', 'full'); 4984 $('.single-page_content-container').attr('data-view', 'full'); 4985 setupVideoPreviews(false); 4986 shouldInsertAd && ((window.AdProvider = window.AdProvider || []).push({ serve: {} }), window.pushStreamTags?.()); 4987 } 4988 4989 const initialize = () => { 4990 const initialViewMode = $('.single-page_content-container').attr('m'); 4991 if (initialViewMode === 'full' && window.innerWidth <= 480){ 4992 $('.media-list-item img:first-child').remove(); 4993 $('.media-list-item img:first-child').show(); 4994 switchToFullView(); 4995 } else { 4996 switchToCompactView(); 4997 $('.media-list-item img:nth-child(2)').remove(); 4998 } 4999 $('.single-page_content-container').removeAttr('m'); 5000 $viewToggleButton.on('click', () => 5001 $viewToggleButton.attr('data-view') === 'compact' ? switchToFullView() : switchToCompactView() 5002 ); 5003 }; 5004 return { initialize, activePlayerIds }; 5005 })(); 5006 5007 /* âââââââââââââââââââââââ 6. Media Type Picker ââââââââââââââââââââââââââ */ 5008 const MediaTypePicker = (() => { 5009 const $pickerButtons = $('.media__picker-button, #media-all, #media-video, #media-image'); 5010 if (!$pickerButtons.length) return { initialize() {} }; 5011 const $videoItems = $('a[data-type="video"]').parents('.media-list-item'); 5012 const $photoItems = $('a[data-type="pic"]').parents('.media-list-item'); 5013 const initialize = () => $pickerButtons.on('click', async function() { 5014 const buttonId = $(this).attr('id'); 5015 if (buttonId.includes('picker')) $pickerButtons.removeClass('media__picker-button--active'); 5016 if (['picker-video', 'media-video'].includes(buttonId)) { 5017 $photoItems.hide(); 5018 $videoItems.not('[view-more]').show(); 5019 } else if (['picker-photo', 'media-image'].includes(buttonId)) { 5020 $videoItems.hide(); 5021 $photoItems.not('[view-more]').show(); 5022 } else if (['picker-cast', 'picker-guide'].includes(buttonId)) { 5023 5024 } else { 5025 $videoItems.not('[view-more]').show(); 5026 $photoItems.not('[view-more]').show(); 5027 } 5028 if (buttonId.includes('picker')) $(this).addClass('media__picker-button--active'); 5029 buttonId === 'picker-photo' && ViewModeToggle.activePlayerIds.length && 5030 jwplayer($('#' + ViewModeToggle.activePlayerIds[0]).parents('.player-position').children().first()[0]).pause(); 5031 $('.watch-all-btn').each(function() { 5032 const visibleItems = $(this).parents('section').find('.media-list-item') 5033 .filter((_, element) => element.style.display !== 'none').length; 5034 $(this).toggle(!!visibleItems); 5035 }); 5036 MediaGallery.initialize(); 5037 }); 5038 return { initialize }; 5039 })(); 5040 5041 /* âââââââââââââââââââââââ 7. Filter Modal Popup âââââââââââââââââââââââââ */ 5042 const FilterModalPopup = (() => { 5043 let $filterButton, $filterModal, $sections; 5044 5045 const initialize = () => { 5046 $filterButton = $('#filter'); 5047 $filterModal = $('#filter-modal'); 5048 if (!$filterButton.length) return; 5049 let vCount= parseInt($('#picker-video').text().replace('Video', '')); 5050 let pCount= parseInt($('#picker-photo').text().replace('Photo', '')); 5051 $sections = $("section"); 5052 $('#search').addClass('svg_btn_off'); 5053 if ($sections.length < 2) { 5054 $("#filter").prop('disabled', true); 5055 $("#filter").addClass('svg_btn_off'); 5056 } 5057 if (vCount === 0){ 5058 $('#picker-video').prop('disabled', true); 5059 $('#picker-video').addClass('button_off'); 5060 } 5061 if (pCount === 0) { 5062 $('#picker-photo').prop('disabled', true); 5063 $('#picker-photo').addClass('button_off'); 5064 } 5065 if ((vCount === 0 || pCount === 0) && vCount + pCount !== 0) { 5066 let vBtn = $('#picker-video'); 5067 let pBtn = $('#picker-photo'); 5068 let parentBtn = vBtn.parents('.media__picker'); 5069 vBtn.detach(); 5070 pBtn.detach(); 5071 if (vCount < pCount) { 5072 parentBtn.append(pBtn); 5073 parentBtn.append(vBtn); 5074 } else { 5075 parentBtn.append(vBtn); 5076 parentBtn.append(pBtn); 5077 } 5078 } 5079 let values_d = $sections; 5080 values_d = Array.from(values_d).filter(x => parseInt($(x).attr('value_d')) <= 5 && parseInt($(x).attr('value_d')) !== -1); 5081 if (!(values_d.length > 0)) { 5082 $('input#5-y').parents('li').remove(); 5083 } 5084 5085 $filterButton.on('click', event => { 5086 event.stopPropagation(); 5087 const buttonRect = $filterButton[0].getBoundingClientRect(); 5088 const positionUp = buttonRect.bottom <= window.innerHeight / 2; 5089 $filterModal.css(positionUp ? { bottom: 'auto', top: '45px', right: '25px' } : { top: 'auto', bottom: '45px', right: '25px' }); 5090 $filterModal.toggleClass('filter-modal--opened').css('display', $filterModal.hasClass('filter-modal--opened') ? 'block' : 'none'); 5091 }); 5092 5093 $(document).on('click', event => { 5094 if (!$filterModal.is(event.target) && !$filterModal.has(event.target).length && $filterModal.hasClass('filter-modal--opened')) { 5095 $filterModal.removeClass('filter-modal--opened').hide(); 5096 } 5097 }); 5098 5099 $filterModal.on('click', event => event.stopPropagation()); 5100 }; 5101 5102 return { initialize }; 5103 })(); 5104 5105 window.FilterModalPopup = FilterModalPopup; 5106 5107 /* âââââââââââââââââââââââ 8. Magnific Popup Gallery âââââââââââââââââââââ */ 5108 MediaGallery = (() => { 5109 const initialize = () => { 5110 // Remove existing magnificPopup bindings to prevent duplicates 5111 $('.media-list-item > a').off('click.magnificPopup'); 5112 $('.media-list-item > a').magnificPopup('destroy'); 5113 5114 // Only select visible items 5115 const $visibleLinks = $('.media-list-item:visible') 5116 .filter(function() { 5117 return !$(this).hasClass('no-hover') &&
5118 $(this).closest('.no-hover').length === 0; 5119 }) 5120 .find('> a'); 5121 5122 const galleryItems = []; 5123 const playerSettings = {}; 5124 let $sharedPlayerContainer; 5125 5126 $visibleLinks.each(function(index) { 5127 const $link = $(this); 5128 const itemTitle = $link.attr('lightbox') || ''; 5129 if ($link.data('type') === 'video') { 5130 const playerId = 'player__' + $link.parent().find('.player-container').find('[id]').attr('id'); 5131 const config = generatePlayerSettings($link.parent().closest('.media-list-item')); 5132 let $innerPlayer = $link.parents('media-list-item').find('.player-container').find('[id^="player"]'); 5133 let vertical = $innerPlayer.attr('s') && $innerPlayer.attr('s') !== ''; 5134 if (vertical) { 5135 config['aspectratio'] = '9:16'; 5136 } 5137 $link.parent().find('.player-container').find('[id]').attr('s') && (config.aspectratio = '9:16'); 5138 const bottomBar = $('<div class="mfp-bottom-bar"/>') 5139 .html(`<div class="mfp-title">${itemTitle}</div><div class="mfp-counter" id="c_${playerId}"></div>`); 5140 const frame = $('<div class="gallery-iframe"/>').append(bottomBar); 5141 config.aspectratio === '9:16' && frame.addClass('vertical'); 5142 galleryItems.push({ 5143 src: frame[0], 5144 type: 'inline', 5145 title: itemTitle, 5146 item_id: playerId, 5147 element_link: this 5148 }); 5149 playerSettings[playerId] = config; 5150 } else { 5151 galleryItems.push({ 5152 src: $link.attr('href'), 5153 type: 'image', 5154 title: itemTitle, 5155 item_id: index, 5156 element_link: this 5157 }); 5158 } 5159 }); 5160 5161 let most_recent_clicked; 5162 $visibleLinks.each(function(i, element) { 5163 $(this).on('click', function() { 5164 let isImg = $(this).hasClass('show-pic'); 5165 most_recent_clicked = isImg; 5166 }) 5167 }); 5168 5169 5170 $visibleLinks.magnificPopup({ 5171 disableOn: function() { 5172 const $el = this.st?.el || $(this); 5173 if ($el.hasClass('no-hover') || $el.closest('.no-hover').length > 0 || $el.parent('.no-hover').length > 0) return false; 5174 if (!((most_recent_clicked && $('#view').attr('data-view') === 'full') || $('#view').attr('data-view') === 'compact')) return false; 5175 return true; 5176 }, 5177 mainClass: 'mfp-fade', 5178 items: galleryItems, 5179 gallery: { enabled: true, navigateByImgClick: true, preload: [0, 0] }, 5180 callbacks: { 5181 beforeOpen: function() { 5182 const $el = this.st?.el; 5183 if ($el && ($el.hasClass('no-hover') || $el.closest('.no-hover').length > 0 || $el.parent('.no-hover').length > 0)) return false; 5184 5185 const $allLinks = $('.media-list-item:visible > a'); 5186 const visibleLinks = $allLinks.toArray(); 5187 this.items = visibleLinks.length 5188 ? galleryItems.filter(item => visibleLinks.includes(item.element_link)) 5189 .sort((a, b) => visibleLinks.indexOf(a.element_link) - visibleLinks.indexOf(b.element_link)) 5190 : []; 5191 const index = visibleLinks.indexOf(this.st.el[0]); 5192 document.body.style.overflow = 'hidden'; 5193 index !== -1 ? this.goTo(index) : this.goTo(0); 5194 }, 5195 change: function() { 5196 // If this is an image, not a video, return!
5197 if (this.currItem.type !== 'inline') return; 5198 5199 // We want to find the parent of the jwplayer, either in prevItem or in the main location 5200 if (this.prevItem) { 5201 // we set the prevItem now and check back if we are on the nextItem (unknown yet) 5202 if (this.currItem.data.item_id !== this.prevItem.data.item_id) { 5203 // 5204 if (AppState.jwPlayerInitialized) { 5205 if ($('#jwplayer_one_container').find('#jwplayer_one').length > 0){ 5206 $sharedPlayerContainer = $('#jwplayer_one_container').find('#jwplayer_one'); 5207 } 5208 // Ok, here now we are on the next item, so we check prevItem for the jwplayer 5209 if ($(this.prevItem.src.firstElementChild).attr('id') === $sharedPlayerContainer.attr('id')) { 5210 // If the prevItem indeed has the player, we update the reference and detach it 5211 $sharedPlayerContainer = $(this.prevItem.src.firstElementChild); 5212 $sharedPlayerContainer.detach(); 5213 } 5214 } 5215 } 5216 } 5217 5218 // the previous code block will only work if we update prevItem manually on each change 5219 this.prevItem = this.currItem; 5220 const $frame = $(this.currItem.src); 5221 5222 // if there is no prevItem or prevItem = currItem, it means the player still needs to be detached 5223 if ($('#jwplayer_one_container').find('#jwplayer_one').length > 0) { 5224 $sharedPlayerContainer = $('#jwplayer_one_container').find('#jwplayer_one'); 5225 $sharedPlayerContainer.detach(); 5226 } 5227 let currFrameChild = $frame[0].firstElementChild; 5228 // we are checking that we have not already prepended the jwplayer (onChange is called twice by design) 5229 if ($(currFrameChild).attr('id') !== $sharedPlayerContainer.attr('id')){ 5230 $frame.prepend($($sharedPlayerContainer)[0]); 5231 currFrameChild = $frame[0].firstElementChild; 5232 } 5233 5234 const playerConfig = { 5235 ...playerSettings[this.currItem.data.item_id], 5236 autostart: true, 5237 autoPause: false, 5238 mute: false 5239 }; 5240 5241 // Because onChange is called twice, this container will be initialized and also updated before rendering, which is fine :D 5242 if(AppState.jwPlayerInitialized) { // Important that we only update the settings/player and not reinitialize 5243 updateJWPlayerConfig(jwplayer(currFrameChild), currFrameChild, playerConfig, AppState.advertisementUrl); 5244 5245 } else { // if jwplayer is not initialized, we'll do that 5246 initializePlayer(playerConfig, jwplayer(currFrameChild), AppState.advertisementUrl, AppState.activeVideoPlayers); 5247 AppState.jwPlayerInitialized = true 5248 } 5249 }, 5250 afterChange: function() { 5251 if (this.currItem.type !== 'inline') return; 5252 const $counter = this.currItem.inlineElement[0].querySelector(`#c_${this.currItem.data.item_id}`); 5253 if ($counter) { 5254 $counter.textContent = `${this.index + 1} of ${this.items.length}`; 5255 } 5256 setTimeout(() => { 5257 try { 5258 const player = jwplayer(this.currItem.src.firstElementChild); 5259 let pstate; 5260 if (player && typeof player.getState === 'function') { 5261 setTimeout(() => { 5262 pstate = player.getState(); 5263 if (!(pstate === 'idle' || pstate === 'buffering' || pstate === 'paused')) return; 5264 setTimeout(() => { 5265 pstate = player.getState(); 5266 if (!(pstate === 'idle' || pstate === 'buffering' || pstate === 'paused')) return; 5267 setTimeout(() => { 5268 pstate = player.getState(); 5269 if (!(pstate === 'idle' || pstate === 'buffering' || pstate === 'paused')) return; 5270 setTimeout(() => { 5271 pstate = player.getState(); 5272 if (!(pstate === 'idle' || pstate === 'buffering' || pstate === 'paused')) return; 5273 if (pstate !== 'playing' && pstate !== 'buffering') { 5274 player.seek(0).play().setVolume(25); 5275 } 5276 }, 75); 5277 }, 100); 5278 }, 100); 5279 }, 100); 5280 } 5281 } catch (e) { 5282 } 5283 }, 200); 5284 }, 5285 beforeClose: function() { 5286 try { 5287 if (!(this.currItem && this.currItem.type === "inline")) return; 5288 // if the player is not in the default location 5289 if (!$('#jwplayer_one_container').find('#jwplayer_one').length > 0) { 5290 let checkpoint = this.currItem.src.firstElementChild; 5291 $(this.currItem.src.firstElementChild).detach(); 5292 $('#jwplayer_one_container').append(checkpoint); 5293 jwplayer(checkpoint).pause(); 5294 } 5295 } catch (e) {} 5296 }, 5297 close: function() { 5298 5299 document.body.style.overflow = ''; 5300 } 5301 } 5302 }); 5303 }; 5304 return { initialize }; 5305 })(); 5306 5307 /* âââââââââââââââââââââââ 9. DOM-Ready Initialization âââââââââââââââââââ */ 5308 document.addEventListener('DOMContentLoaded', async () => { 5309 if (!$('.single-page_content-container').length) return; 5310 const isAdBlockerActive = await (userHasAdBlockerOn?.() || false); 5311 AppState.advertisementUrl = isAdBlockerActive ? `${mainExoDomain}/v1/vast.php?idzone=${exoads}&sub=1&block=1` : `https://s.magsrv.com/v1/vast.php?idzone=${exoads}`; 5312 FilterModalPopup.initialize(); 5313 MediaTypePicker.initialize(); 5314 ViewModeToggle.initialize(); 5315 MediaGallery.initialize(); 5316 }); 5317})();
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.