隐私政策

您的隐私在TTOK.com对我们的隐私非常重要。 此隐私政策文件概述了TTOK.com号数据接收到的信息, 以及我们如何储存、管理和保护这些数据。

日志

TTOK.com 使用日志文件, 日志文件是记录信息的文件, 包括请求的网页资源、 IP 地址、 浏览器类型和时间戳。 TTOK.com 将这些信息用于排除故障、 服务分析和网站维护 。 此信息无法直接连接到个人识别的信息 。 也没有连接到视频或音频 。 IP 地址最多用来记录最多3个完成的录音, 但没有记录。 一小时后过期 。

日志文件及其中的任何数据均不与任何第三方共享。

收集的信息

TTOK.com 接收并存储您在网站上输入的信息,包括请求的资源、搜索查询和网站设置,存储这些信息是为了监测和分析服务活动,无法与个人识别的信息链接。

您输入的所有信息都是自愿的,任何信息均不与第三方共享。

TTOK.com人不会收集任何有关您的个人信息,除非您在知情的情况下具体提供此类信息。

曲曲曲曲曲曲曲曲曲曲曲曲曲

TTOK.com 将您的喜好保存在 cookie 中。 虽然这些 cookie 通过记住您的设置来改善您的经验, 但是它们完全是可选的。 TTOK.com 的所有功能和服务都不用 cookie 就可以工作 。

第三方可以通过嵌入TTOK.com的第三方内容储存饼干,例如多媒体播放器和分析器。 TTOK.com对第三方储存的饼干没有控制权。

问 问 问 问 问 问 问 问 问 问

如果您对这项隐私政策有任何疑问,请自由使用 { const el = input[0]; if (!el) return; el.focus({ preventScroll: false }); const original = el.placeholder; const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform || ''); el.placeholder = isMac ? 'Press ⌘V to paste' : 'Press Ctrl+V to paste'; setTimeout(() => { el.placeholder = original; }, 4000); }; try { if (navigator.clipboard && navigator.clipboard.readText) { const clipboardText = await navigator.clipboard.readText(); if (clipboardText) { input.val(clipboardText).trigger("change"); } else { focusForManualPaste(); } } else { focusForManualPaste(); } } catch (err) { focusForManualPaste(); } }); function initClipSlider(info, duration) { var sliderEl = info.find('.clip-slider')[0]; if (sliderEl.noUiSlider) return; info.find('[data-input=clip-start]').val(0); info.find('[data-input=clip-end]').val(duration); info.find('[data-input=clip-start-display]').val(utils.formatDuration(0)); info.find('[data-input=clip-end-display]').val(utils.formatDuration(duration)); noUiSlider.create(sliderEl, { start: [0, duration], connect: true, step: 0.1, range: {'min': 0, 'max': duration}, behaviour: 'tap-drag' }); sliderEl.noUiSlider.on('update', function (values, handle) { var sec = parseFloat(values[handle]); if (handle === 0) { info.find('[data-input=clip-start]').val(sec); info.find('[data-input=clip-start-display]').val(utils.formatDuration(sec)); } else { info.find('[data-input=clip-end]').val(sec); info.find('[data-input=clip-end-display]').val(utils.formatDuration(sec)); } }); sliderEl.noUiSlider.on('change', function () { resetDownloadButton(info); }); info.find('[data-input=clip-start-display]').on('change', function () { var sec = utils.parseDuration($(this).val()); var maxVal = parseFloat(info.find('[data-input=clip-end]').val()); sec = Math.max(0, Math.min(sec, maxVal)); sliderEl.noUiSlider.set([sec, null]); $(this).val(utils.formatDuration(sec)); }); info.find('[data-input=clip-end-display]').on('change', function () { var sec = utils.parseDuration($(this).val()); var minVal = parseFloat(info.find('[data-input=clip-start]').val()); sec = Math.max(minVal, Math.min(sec, duration)); sliderEl.noUiSlider.set([null, sec]); $(this).val(utils.formatDuration(sec)); }); } container.on("change", "[data-input=clip-toggle]", function () { var info = $(this).closest('.result-item-info'); var clipInputs = info.find('.clip-time-inputs'); if (this.checked) { if (!hasPro) { this.checked = false; var modal = new bootstrap.Modal(document.getElementById('upgradeModal')); modal.show(); return; } clipInputs.show(); var cachedDuration = info.data('duration'); if (cachedDuration) { info.find('.clip-fields').show(); info.find('.clip-loading').hide(); initClipSlider(info, cachedDuration); } else { var attrDuration = info.attr('data-duration'); if (attrDuration && parseFloat(attrDuration) > 0) { var dur = parseFloat(attrDuration); info.data('duration', dur); info.find('.clip-fields').show(); info.find('.clip-loading').hide(); initClipSlider(info, dur); } else { info.find('.clip-loading').show(); info.find('.clip-fields').hide(); info.find('.clip-error').hide(); var itemPageUrl = info.attr('data-item-page-url') || ''; var pageUrl = decodeURIComponent(info.find('.download-button').data('page-url') || ''); var directUrl = decodeURIComponent(info.find('.download-button').data('url') || ''); var fetchUrl = itemPageUrl || pageUrl || directUrl; $.ajax({ url: '/api/duration/', type: 'POST', data: { url: fetchUrl, csrfmiddlewaretoken: 'og7JpJ5LmNVQL40g2lhdjvUDouxtOUuahztPVGW1EfHMjKDkwAlxBeefCJPvUdHn' }, success: function (data) { if (data.duration) { info.data('duration', data.duration); info.find('.clip-loading').hide(); info.find('.clip-fields').show(); initClipSlider(info, data.duration); } else { info.find('.clip-loading').hide(); info.find('.clip-error').text('Could not get duration').show(); } }, error: function () { info.find('.clip-loading').hide(); info.find('.clip-error').text('Could not get duration').show(); } }); } } } else { clipInputs.hide(); } resetDownloadButton(info); }); container.on("click", ".download-button", function (e) { var target = $(this); var info = target.closest('.result-item-info'); var itemType = target.data('type') || ''; var format = info.find('[data-input=format]:checked').val() || ''; var quality = info.find('[data-input=quality]').val() || ''; var h264 = info.find('input[name=h264]').is(':checked'); // Image conversion (Pro only) if (itemType === 'image' && GO_API_URL) { var imgFormat = info.find('[data-input=img-format]:checked').val() || ''; var imgWidth = info.find('[data-input=img-width]').val() || ''; var imgHeight = info.find('[data-input=img-height]').val() || ''; var imgAspectRatio = info.find('[data-input=img-aspect-ratio]').val() || ''; var needsImageConversion = imgFormat !== '' || imgWidth !== '' || imgHeight !== '' || imgAspectRatio !== ''; if (needsImageConversion) { e.preventDefault(); if (!hasPro) { var modal = new bootstrap.Modal(document.getElementById('upgradeModal')); modal.show(); return; } var sourceUrl = decodeURIComponent(target.data('url')); var fn = target.data('fn') || 'image'; var form = document.createElement('form'); form.method = 'POST'; form.action = '/api/convert/image/'; form.style.display = 'none'; function addImgField(name, value) { var input = document.createElement('input'); input.type = 'hidden'; input.name = name; input.value = value; form.appendChild(input); } addImgField('csrfmiddlewaretoken', 'og7JpJ5LmNVQL40g2lhdjvUDouxtOUuahztPVGW1EfHMjKDkwAlxBeefCJPvUdHn'); addImgField('url', sourceUrl); addImgField('title', fn); if (imgFormat) addImgField('image_format', imgFormat); if (imgWidth) addImgField('width', imgWidth); if (imgHeight) addImgField('height', imgHeight); if (imgAspectRatio) addImgField('aspect_ratio', imgAspectRatio); document.body.appendChild(form); form.submit(); document.body.removeChild(form); onDownloadButton.call(target[0]); return; } } var needsConversion = (format === 'wav') || (quality !== '') || h264; if (needsConversion && !hasPro) { e.preventDefault(); var modal = new bootstrap.Modal(document.getElementById('upgradeModal')); modal.show(); return; } if (needsConversion && GO_API_URL) { e.preventDefault(); var pageUrl = decodeURIComponent(target.data('page-url') || ''); var fn = target.data('fn') || 'download'; // For TikTok: use proxy URL as source (yt-dlp can't extract TikTok) var sourceUrl = pageUrl; if (pageUrl.indexOf('tiktok.com') !== -1) { var proxyType = (itemType === 'audio') ? 'audio' : 'video'; sourceUrl = GO_API_URL + '/api/proxy?url=' + encodeURIComponent(pageUrl) + '&type=' + proxyType + '&fn=' + encodeURIComponent(fn); } var fmt; if (format === 'wav') { fmt = 'wav'; } else if (itemType === 'audio') { fmt = 'mp3'; } else { fmt = 'mp4'; } // POST via hidden form to Django proxy (same-origin, streams from Go binary) var form = document.createElement('form'); form.method = 'POST'; form.action = '/api/convert/' + fmt + '/'; form.style.display = 'none'; function addField(name, value) { var input = document.createElement('input'); input.type = 'hidden'; input.name = name; input.value = value; form.appendChild(input); } addField('csrfmiddlewaretoken', 'og7JpJ5LmNVQL40g2lhdjvUDouxtOUuahztPVGW1EfHMjKDkwAlxBeefCJPvUdHn'); addField('url', sourceUrl); addField('title', fn); if (fmt === 'mp3' && quality) addField('audio_quality', quality + 'k'); if (fmt === 'mp4' && quality) addField('video_quality', quality); if (fmt === 'mp4' && h264) addField('h264', 'true'); document.body.appendChild(form); form.submit(); document.body.removeChild(form); onDownloadButton.call(this); } else { onDownloadButton.call(this); } }); container.on("change", "[data-input=format]", function () { var info = $(this).closest('.result-item-info'); var row = info.find('.quality-row'); if ($(this).val() === 'wav') { row.addClass("d-none"); } else { row.removeClass("d-none"); } resetDownloadButton(info); }); container.on("change input", "[data-input=quality]", function () { resetDownloadButton($(this).closest('.result-item-info')); }); container.on("change", "input[name=h264]", function () { resetDownloadButton($(this).closest('.result-item-info')); }); onChangeURLValue(); function onDownloadAll() { var target = $(this); target.find(".spinner-border").removeClass("d-none"); target.prop("disabled", true); var filter = container.find('input[name="downloadFilter"]:checked').val() || 'all'; var downloadLinks = []; var downloadButtons = container.find(".download-button"); downloadButtons.each(function (index, item) { var type = $(item).data("type") || ""; if (filter === "all" || type === filter || (filter === "video" && (type === "video" || type === "video_watermark"))) { downloadLinks.push($(item).data("url")); $(item).addClass("downloaded"); } }); if (downloadLinks.length === 0) { target.find(".spinner-border").addClass("d-none"); target.prop("disabled", false); return; } var batchDownloadI18n = { intro: "We detected {count} files. To keep things reliable we will download them in {n} batches of up to {size}. Each batch is saved separately as it finishes.", progress: "Batch {current} of {total}", retrying: "Batch {batch} failed, retrying in {sec}s…", failed: "Batch {batch} failed after retries. Press Resume to try again.", done_status: "All batches downloaded.", resume_prompt: "You already downloaded {done} of {total} batches for this list. Continue from there?", cancel_confirm: "Cancel the remaining batches?" }; batchDownload.show({ urls: downloadLinks, baseName: "TTOK", csrfToken: "og7JpJ5LmNVQL40g2lhdjvUDouxtOUuahztPVGW1EfHMjKDkwAlxBeefCJPvUdHn", goApiUrl: GO_API_URL || "", i18n: batchDownloadI18n, onAllDone: function () { setTimeout(function () { target.find(".spinner-border").addClass("d-none"); target.text('分享TToK.com'); target.removeClass('btn-primary').addClass('btn-success'); target.prop("disabled", false); launchConfetti(target, 150, 2200); }, 100); }, onError: function (err) { alert(err && err.message ? err.message : err); target.find(".spinner-border").addClass("d-none"); target.prop("disabled", false); }, onCancel: function () { target.find(".spinner-border").addClass("d-none"); target.prop("disabled", false); } }); } var individualDownloadCount = 0; function onDownloadButton() { var target = $(this); if (!target.data('original-html')) { target.data('original-html', target.html()); target.data('original-class', target.attr('class')); } target.find(".spinner-border").removeClass("d-none"); setTimeout(function () { target.find(".spinner-border").addClass("d-none"); target.text('分享TToK.com'); target.removeClass('btn-outline-primary').addClass('btn-success'); launchConfetti(target, 150, 2200); }, 100) if (!hasPro) { individualDownloadCount++; var banner = $('#batchUpsellBanner'); if (banner.length && individualDownloadCount >= 3) { $('#batchUpsellCount').text(individualDownloadCount); banner.removeClass('d-none'); } } } function resetDownloadButton(info) { var btn = info.find('.download-button'); var originalHtml = btn.data('original-html'); if (originalHtml) { btn.html(originalHtml); btn.attr('class', btn.data('original-class')); } } // Multi-select for batch download container.on('change', '.select-item-checkbox', function () { var checked = container.find('.select-item-checkbox:checked'); var count = checked.length; if (!hasPro && count > 1) { $(this).prop('checked', false); var modal = new bootstrap.Modal(document.getElementById('upgradeModal')); modal.show(); return; } $('#selectedCount').text(count); if (count > 1) { $('#downloadSelectedBtn').removeClass('d-none'); } else { $('#downloadSelectedBtn').addClass('d-none'); } }); $('#downloadSelectedBtn').on('click', function () { var target = $(this); target.find('.spinner-border').removeClass('d-none'); target.prop('disabled', true); var selectedUrls = []; container.find('.select-item-checkbox:checked').each(function () { selectedUrls.push($(this).data('url')); }); if (selectedUrls.length === 0) { target.find('.spinner-border').addClass('d-none'); target.prop('disabled', false); return; } var batchDownloadI18n = { intro: "We detected {count} files. To keep things reliable we will download them in {n} batches of up to {size}. Each batch is saved separately as it finishes.", progress: "Batch {current} of {total}", retrying: "Batch {batch} failed, retrying in {sec}s…", failed: "Batch {batch} failed after retries. Press Resume to try again.", done_status: "All batches downloaded.", resume_prompt: "You already downloaded {done} of {total} batches for this list. Continue from there?", cancel_confirm: "Cancel the remaining batches?" }; batchDownload.show({ urls: selectedUrls, baseName: "TTOK-selected", csrfToken: "og7JpJ5LmNVQL40g2lhdjvUDouxtOUuahztPVGW1EfHMjKDkwAlxBeefCJPvUdHn", goApiUrl: GO_API_URL || "", i18n: batchDownloadI18n, onAllDone: function () { target.find('.spinner-border').addClass('d-none'); target.prop('disabled', false); launchConfetti(target, 150, 2200); }, onError: function (err) { alert(err && err.message ? err.message : 'An error occurred'); target.find('.spinner-border').addClass('d-none'); target.prop('disabled', false); }, onCancel: function () { target.find('.spinner-border').addClass('d-none'); target.prop('disabled', false); } }); }); function launchConfetti($btn, amount, lifetime) { var colors = ['#FF4757', '#2ED573', '#1E90FF', '#FFA502', '#A55EEA', '#2ECC71', '#FF6B81']; var offset = $btn.offset(); var btnWidth = $btn.outerWidth(); var btnHeight = $btn.outerHeight(); for (var i = 0; i < amount; i++) { var $c = $(''); var size = 3 + Math.random() * 4; var dur = 0.8 + Math.random() * 0.8; var delay = Math.random() * 0.2; var color = colors[Math.floor(Math.random() * colors.length)]; var rotateStart = Math.floor(Math.random() * 360); var startLeft = offset.left + Math.random() * btnWidth; var startTop = offset.top + Math.random() * btnHeight; $c.css({ position: 'absolute', left: startLeft + 'px', top: startTop + 'px', background: color, width: size + 'px', height: (size + 2) + 'px', transform: 'rotate(' + rotateStart + 'deg)', animation: 'fall linear forwards', animationDuration: dur + 's', animationDelay: delay + 's', zIndex: 9999, pointerEvents: 'none', opacity: 0.9 }); $('body').append($c); (function (el) { setTimeout(function () { el.remove(); }, lifetime || 2000); })($c); } } function onChangeURLValue() { const inputValue = $('#url').val(); if (!inputValue) { return } const button = $('#copyPasteText'); const pasteText = button.data('paste'); const clearText = button.data('clear'); if (inputValue.trim() === '') { button.text(pasteText); } else { button.text(clearText); } } function onLoadMore() { var wrapper = $(this).closest(".job-wrapper"); container.find("#loadMoreEntries").prop("disabled", true); container.find("#loadMoreEntries").find(".spinner-border").removeClass("d-none"); var target = container.find("#form"); var formData = target.serializeArray(); var nextPageToken = wrapper.find("[name=next_page_token]").val(); formData.push({name: 'page', value: page}); formData.push({name: 'next_page_token', value: nextPageToken}); $.ajax({ url: `/api/extract/`, type: 'POST', data: formData, success: function (data) { if (data.error === true || data.success === false) { $('.m-progress').removeClass('m-progress').removeAttr('disabled'); container.find("#loadMoreEntries .spinner-border").addClass("d-none"); container.find("#loadMoreEntries").prop("disabled", false); if (data.html) { wrapper.find(".job-result").append(data.html); } return; } wrapper.find("#actionsWrapper").remove(); wrapper.find(`.job-result #postListWrapper`).append(data.html); wrapper.find(`[name=next_page_token]`).val(data.next_page_token); if (!data.next_page_token) { container.find("#loadMoreEntries").remove(); } else { container.find("#loadMoreEntries .spinner-border").addClass("d-none"); container.find("#loadMoreEntries").removeClass('m-progress').removeAttr('disabled'); } initializeLazyLoad(); page += 1; }, error: function (xhr, status, error) { container.find("#loadMoreEntries .spinner-border").addClass("d-none"); container.find("#loadMoreEntries").prop("disabled", false); try { var response = JSON.parse(xhr.responseText); if (response.html) { wrapper.find(".job-result").append(response.html); } } catch (e) { alert(error); } } }); } async function resolveShortUrl(url) { // Try to resolve TikTok short URLs client-side // This works because fetch from user's browser isn't rate-limited like server try { const response = await fetch(url, { method: 'HEAD', redirect: 'follow' }); // If we got here, response.url contains the final URL if (response.url && response.url.includes('/video/') || response.url.includes('/photo/')) { return response.url; } } catch (e) { // CORS error - expected, but browser still followed redirect // Try alternative: open in hidden iframe approach won't work // Fall back to letting server handle it } return url; // Return original if can't resolve } function isTikTokShortUrl(url) { return url.match(/^https?:\/\/(vt|vm)\.tiktok\.com\//i); } async function onSubmit(e) { page = 1; var target = $(this); container.find("#result").html(""); container.find("#heading").html(""); e.preventDefault(); var urlInput = target.find("#url").val(); if (!isValidURL(urlInput)) { alert("请输入有效的 URL"); return; } // Try to resolve TikTok short URLs client-side to avoid server rate limits if (isTikTokShortUrl(urlInput)) { target.find("button").addClass("m-progress").attr('disabled', 'disabled'); try { var resolvedUrl = await resolveShortUrl(urlInput); if (resolvedUrl !== urlInput) { target.find("#url").val(resolvedUrl); urlInput = resolvedUrl; } } catch (e) { // Continue with original URL } target.find("button").removeClass("m-progress").removeAttr('disabled'); } target.find("button").addClass("m-progress").attr('disabled', 'disabled'); // Show loader var wrapperId = 'extract-' + Date.now(); var wrapperHtml = "

"; container.find("#result").append(wrapperHtml); container.find("#" + wrapperId + " .job-result").append($(loader)); $.ajax({ url: `/api/extract/`, type: 'POST', data: target.serialize(), success: function (data) { if (data.error === true || data.success === false) { $('.m-progress').removeClass('m-progress').removeAttr('disabled'); if (data.html) { container.find("#" + wrapperId + " .job-result").html(data.html); } else { container.find("#" + wrapperId + " .job-result").html( '
出现未知错误。
' ); } return; } container.find("#heading").html(data.heading); container.find("#" + wrapperId + " .job-result").html(data.html); container.find("#" + wrapperId + " [name=next_page_token]").val(data.next_page_token); if (!data.next_page_token) { container.find("#loadMoreEntries").remove(); } $('.m-progress').removeClass('m-progress').removeAttr('disabled'); initializeLazyLoad(); page += 1; }, error: function (xhr, status, error) { $('.m-progress').removeClass('m-progress').removeAttr('disabled'); try { var errorJSON = JSON.parse(xhr.responseText); // Rate limit error if (errorJSON.rate_limit || errorJSON.no_credits) { container.find("#" + wrapperId).remove(); container.find("#rateModal .exceeded-wrapper").html(errorJSON.error); container.find('#rateModal').modal('show'); if (errorJSON.until) { var rateInterval; var totalSeconds = errorJSON.until; container.find('.time-exceeded').text(utils.formatDuration(totalSeconds)); rateInterval = setInterval( function () { totalSeconds--; container.find('.time-exceeded').text(utils.formatDuration(totalSeconds)); if (totalSeconds < 0) { clearInterval(rateInterval); window.location.reload(); } }, 1000 ); } return; } // Bulk URL error if (errorJSON.bulk === false) { container.find("#" + wrapperId).remove(); container.find("#rateModal .exceeded-wrapper").html(errorJSON.error); container.find('#rateModal').modal('show'); return; } // Other error with HTML if (errorJSON.html) { container.find("#" + wrapperId + " .job-result").html(errorJSON.html); } else if (errorJSON.error) { container.find("#" + wrapperId + " .job-result").html( '
' + errorJSON.error + '
' ); } } catch (e) { container.find("#" + wrapperId + " .job-result").html( '
处理请求时发生错误。
' ); } } }); } function isValidURL(string) { try { // Extraer solo la parte antes de los parámetros (?) let baseUrl = string.split("?")[0]; // Intentar crear un objeto URL para validar let url = new URL(baseUrl); // Asegurar que el hostname (dominio) es válido return !!(url.hostname && url.hostname.includes(".")); } catch (e) { return false; // Si la URL no es válida, devolver false } } function getResults(jobId, $wrapper, onComplete, next_page_token) { $.ajax({ url: `/api/result/`, type: 'GET', data: { job_id: jobId, nextPageToken: next_page_token, }, success: function (data) { if (data.loading) { setTimeout(function () { getResults(jobId, $wrapper, onComplete, next_page_token); }, 4000); return; } // Check if response indicates failure (even with status 200) // This happens when job completes but with errors if (data.error === true || data.success === false) { $('.m-progress').removeClass('m-progress').removeAttr('disabled'); if (data.html) { container.find(`#${jobId} .job-result`).html(data.html); } else { container.find(`#${jobId} .job-result`).html( '
出现未知错误。
' ); } if (onComplete) onComplete(); return; } container.find("#heading").html(data.heading); if (page > 1) { $wrapper.find("#actionsWrapper").remove(); $wrapper.find(`.job-result #postListWrapper`).append(data.html); $wrapper.find(`[name=next_page_token]`).val(data.next_page_token); } else { container.find(`#${jobId} .job-result`).html(data.html); container.find(`#${jobId} [name=next_page_token]`).val(data.next_page_token); } if (!data.next_page_token) { container.find("#loadMoreEntries").remove(); } else { container.find("#loadMoreEntries .spinner-border").addClass("d-none"); container.find("#loadMoreEntries").removeClass('m-progress').removeAttr('disabled'); } $('.m-progress').removeClass('m-progress').removeAttr('disabled'); initializeLazyLoad(); page += 1; if (onComplete) onComplete(); }, error: function (xhr, status, error) { $('.m-progress').removeClass('m-progress').removeAttr('disabled'); try { const response = JSON.parse(xhr.responseText); if (response.html) { container.find(`#${jobId} .job-result`).html(response.html); } else { container.find(`#${jobId} .job-result`).html( '
出现未知错误。
' ); } } catch (e) { container.find(`#${jobId} .job-result`).html( '
处理请求时发生错误。
' ); } if (onComplete) onComplete(); } }); } function initializeLazyLoad() { const lazyImages = document.querySelectorAll('img.lazyload[data-src]'); if (!('IntersectionObserver' in window)) { // Fallback simple para navegadores antiguos lazyImages.forEach(img => { img.src = img.getAttribute('data-src'); img.removeAttribute('data-src'); img.classList.remove('lazyload'); }); return; } const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (!img.getAttribute('data-src')) return; img.src = img.getAttribute('data-src'); img.removeAttribute('data-src'); img.onload = () => { const parent = img.closest('.thumbnail-container'); const loader = parent ? parent.querySelector('.loader-gif') : null; if (loader) { loader.style.display = 'none'; } }; img.classList.remove('lazyload'); observer.unobserve(img); } }); }); lazyImages.forEach(img => observer.observe(img)); // Initialize Bootstrap tooltips on newly loaded content document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) { if (!bootstrap.Tooltip.getInstance(el)) { new bootstrap.Tooltip(el); } }); } } ); }()); // Exit-intent popup for non-PRO users (function () { var exitShown = false; document.addEventListener('mouseout', function (e) { if (e.clientY > 0 || exitShown || sessionStorage.getItem('exitShown')) return; if (e.relatedTarget || e.toElement) return; exitShown = true; sessionStorage.setItem('exitShown', '1'); var exitModal = document.getElementById('exitIntentModal'); if (exitModal) { var modal = new bootstrap.Modal(exitModal); modal.show(); } }); })();