您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automates Instagram scrolling: fluid in foreground, jumpy in background. Detects and filters sample offers by popular beauty brands, and auto-likes relevant posts. Includes customizable settings and improved UI. Now with in-app Help and Update options in settings, improved modal visibility, fluid UI panel animations, intelligent loading detection, and draggable UI confined to viewport.
当前为
// ==UserScript== // @name Instagram Sample Bot (Adaptive Scroll, Sample & Like) // @namespace http://tampermonkey.net/ // @version 1.32 // @description Automates Instagram scrolling: fluid in foreground, jumpy in background. Detects and filters sample offers by popular beauty brands, and auto-likes relevant posts. Includes customizable settings and improved UI. Now with in-app Help and Update options in settings, improved modal visibility, fluid UI panel animations, intelligent loading detection, and draggable UI confined to viewport. // @author dprits419 // @match https://www.instagram.com/* // @run-at document-idle // @grant none // ==/UserScript== (function() { 'use strict'; // IMPORTANT: Make sure this version matches the @version in the header! const SCRIPT_VERSION = "1.32"; // --- IMPORTANT: CONFIGURE THESE URLs IF YOU HOST YOUR SCRIPT --- // This is the URL where your raw .user.js script is hosted. // When the "Update Userscript" button is clicked, it opens this URL. // Tampermonkey will detect the .user.js file and prompt for update if a newer version is available. const SCRIPT_INSTALL_URL = 'https://update.greasyfork.org/scripts/538055/Instagram%20Sample%20Bot%20%28Adaptive%20Scroll%2C%20Sample%20%20Like%29.user.js'; // EXAMPLE: Replace with your actual GitHub Gist raw URL or Greasy Fork URL // This is the URL for a help page or README for your script. // NOTE: This URL is now primarily for reference, as help is shown in-app. const SCRIPT_HELP_URL = 'https://update.greasyfork.org/scripts/538055/Instagram%20Sample%20Bot%20%28Adaptive%20Scroll%2C%20Sample%20%20Like%29.user.js'; // EXAMPLE: Replace with your actual help/documentation URL // --- END CONFIGURATION --- // Processing parameters (these are not customizable via UI for now) const processPostsInterval = 2500; // How often to process posts (liking, sample detection) const endOfFeedConfirmDelay = 1500; // Milliseconds to confirm end of feed after hitting bottom console.log(`Instagram Bot script loaded! (Version ${SCRIPT_VERSION} - Draggable UI Confined to Viewport)`); // Global settings object with defaults let settings = { pixelsPerFrame: 4, // For fluid scrolling (foreground) jumpyScrollInterval: 1000, // How often to jump in jumpy mode (background) enableLiking: true, // Toggle auto-liking enableSampleDetection: true, // Toggle sample offer detection minConfidenceThreshold: 1.5, // Confidence required for sample detection acceptOnlyPopularBrands: false, // Filter sample offers by popular brands }; // Global state for bot activity let scrolling = false; // References to the two types of scrolling mechanisms let fluidAnimationFrameId = null; // For requestAnimationFrame let jumpyScrollIntervalId = null; // For setInterval (background) let processPostsIntervalId = null; // Separate interval for processing posts // Track last known scroll positions for end-of-feed detection let lastScrollY = 0; let lastScrollHeight = 0; // --- Variables for Stuck/Loading Detection --- let stuckDetectionIntervalId = null; // For hard refresh let lastEffectiveScrollTime = Date.now(); // Timestamp of last *effective* scroll change (for hard stuck detection) const STUCK_TIMEOUT_MS = 30 * 1000; // 30 seconds without *any* scroll progress before full refresh const AUTO_START_FLAG = 'instagramBotAutoStartAfterReload'; // Flag for localStorage let noScrollProgressTimeout = null; // Timeout for initial detection of no scroll progress const NO_SCROLL_PROGRESS_WARN_DELAY = 3 * 1000; // 3 seconds without scroll progress to warn/slow down initially let isTemporarilyStuckLoading = false; // Flag to indicate if we're in the slow-down state let originalPixelsPerFrame = settings.pixelsPerFrame; // Store original for restoration let originalJumpyScrollInterval = settings.jumpyScrollInterval; // Store original for restoration const SLOW_SCROLL_FACTOR_FLUID = 0.5; // Reduce fluid speed to 50% const SLOW_SCROLL_FACTOR_JUMPY = 2.0; // Double jumpy interval (half speed) // Keywords for Sample Detection // General sample-related keywords (will be used with negative phrases for context) const sampleKeywords = ['free', 'sample', 'samples', 'offer', 'deal', 'giveaway', 'trial', 'complimentary', 'discount', 'coupon', 'win', 'contest', 'promo', 'gift']; // Phrases that strongly indicate an offer (higher confidence) const strongOfferPhrases = [ 'get your free', 'claim your free', 'win a free', 'enter to win', 'get your sample', 'claim your sample', 'free product', 'free gift', 'limited time offer', 'exclusive offer', 'sign up for free', 'redeem your', 'grab your' ]; const sampleActionTexts = ['sign up', 'learn more', 'shop now', 'claim here', 'get sample', 'redeem', 'click here', 'get offer', 'apply now']; // Negative keywords/phrases to reduce false positives by penalizing confidence // These are very specific non-offer contexts for 'free' or 'sample'. const negativeSamplePhrases = [ 'cruelty-free', 'free from', 'gluten-free', 'sugar-free', 'dairy-free', 'ad-free', 'sample size', 'sample of work', 'free to use', 'free to download', 'free trial', 'sample chapter', 'sample lesson', 'sample video', 'sample audio', 'sample data', 'sample image', 'sample text', 'free consultation', 'free estimate' ]; // Keywords for Auto-Liking const cologneKeywords = ['cologne', 'fragrance', 'perfume', 'scent', 'eau de parfum', 'eau de toilette']; const beautyKeywords = [ 'skincare', 'makeup', 'cosmetics', 'beauty', 'haircare', 'dior', 'chanel', 'sephora', 'ulta', 'fenty', 'kylie cosmetics', 'nars', 'mac cosmetics', 'glossier', 'rare beauty', 'lancome', 'este lauder', 'clinique', 'shiseido', 'hourglass', 'charlotte tilly', 'tatcha', 'kiehl\'s', 'cerave', 'la roche-posay', 'olaplex', 'sol de janeiro', 'lip gloss', 'mascara', 'eyeshadow', 'blush', 'foundation', 'concealer', 'serum', 'moisturizer', 'cleanser', 'sunscreen' ]; // List of popular beauty brands for filtering const popularBeautyBrands = [ 'sephora', 'ulta', 'dior', 'chanel', 'fenty beauty', 'kylie cosmetics', 'nars', 'mac cosmetics', 'glossier', 'rare beauty', 'lancome', 'este lauder', 'clinique', 'shiseido', 'hourglass', 'charlotte tilly', 'tatcha', 'kiehl\'s', 'cerave', 'la roche-posay', 'olaplex', 'sol de janeiro', 'anastasia beverly hills', 'too faced', 'urban decay', 'benefit cosmetics', 'tarte', 'it cosmetics', 'fresh', 'sunday riley', 'drunk elephant', 'summer fridays', 'glow recipe', 'paula\'s choice', 'the ordinary', 'skinfix', 'dr. jart+', 'biossance', 'supergoop', 'milk makeup', 'kosas', 'saie', 'tower 28', 'innisfree', 'laneige', 'dr. brandt', 'peter thomas roth', 'first aid beauty', 'farmacy', 'youth to the people', 'herbivore botanicals', 'origins', 'clarins', 'guerlain', 'sisley', 'la mer', 'skinceuticals', 'obagi', 'elizabeth arden', 'aveeno', 'neutrogena', 'cetaphil', 'vichy', 'laroche-posay' ]; let statusBarElement; // Reference to the status bar element // --- Settings Management --- function loadSettings() { try { const savedSettings = JSON.parse(localStorage.getItem('instagramBotSettings')); if (savedSettings) { // Merge saved settings with defaults to ensure new settings are added but old ones persist settings = { ...settings, ...savedSettings }; } // Update original values based on loaded settings originalPixelsPerFrame = settings.pixelsPerFrame; originalJumpyScrollInterval = settings.jumpyScrollInterval; console.log("Settings loaded:", settings); } catch (e) { console.error("Error loading settings from localStorage:", e); } } function saveSettings() { try { localStorage.setItem('instagramBotSettings', JSON.stringify(settings)); updateStatus("Settings saved!", 'var(--primary-color)'); console.log("Settings saved:", settings); // Also update original values if settings are saved originalPixelsPerFrame = settings.pixelsPerFrame; originalJumpyScrollInterval = settings.jumpyScrollInterval; } catch (e) { console.error("Error saving settings to localStorage:", e); updateStatus("Failed to save settings!", 'var(--danger-color)'); } } // --- Utility Functions --- // Helper to update the status bar function updateStatus(message, color = 'var(--text-color, #333)') { if (statusBarElement) { statusBarElement.innerText = message; statusBarElement.style.color = color; } console.log("STATUS: " + message); } // Helper for delays function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Helper to detect if a post is sponsored function isPostSponsored(postElement) { const sponsoredIndicator = postElement.querySelector('span[aria-label="Sponsored"], div[aria-label="Sponsored"], [data-testid="sponsored-label"], span:-webkit-any(span, div)[tabindex="-1"][role="button"]'); if (sponsoredIndicator && (sponsoredIndicator.innerText.toLowerCase().includes('sponsored') || sponsoredIndicator.getAttribute('aria-label') === 'Sponsored')) { return true; } return false; } // --- Liking Logic --- async function likePost(postElement) { const unlikeButton = postElement.querySelector('svg[aria-label="Unlike"]')?.closest('button'); if (unlikeButton) { return false; // Already liked } const likeButton = postElement.querySelector('svg[aria-label="Like"]')?.closest('button'); if (likeButton) { likeButton.click(); return true; // Successfully clicked like button } return false; // Like button not found } // --- Scrolling Mode Management --- // Starts continuous, fluid scrolling using requestAnimationFrame function startFluidScrolling() { if (fluidAnimationFrameId) return; // Already fluid scrolling // Ensure jumpy scrolling is stopped if (jumpyScrollIntervalId) { clearInterval(jumpyScrollIntervalId); jumpyScrollIntervalId = null; } function animateScroll() { if (!scrolling) return; // Stop if scrolling is cancelled const currentScrollY = window.scrollY; const currentScrollHeight = document.documentElement.scrollHeight; if (currentScrollY === lastScrollY && currentScrollHeight === lastScrollHeight) { // No scroll progress if (!noScrollProgressTimeout) { // Start a timeout to detect prolonged lack of scroll progress noScrollProgressTimeout = setTimeout(() => { if (window.scrollY === lastScrollY && document.documentElement.scrollHeight === lastScrollHeight) { // Still no scroll progress after initial delay, slow down isTemporarilyStuckLoading = true; settings.pixelsPerFrame = Math.max(1, Math.round(originalPixelsPerFrame * SLOW_SCROLL_FACTOR_FLUID)); updateStatus(`Slowing for loading (${settings.pixelsPerFrame}px/frame)...`, 'orange'); } noScrollProgressTimeout = null; // Clear this timeout regardless if it triggered slow down or not }, NO_SCROLL_PROGRESS_WARN_DELAY); } // If we are in a slowed-down state, or just detected no scroll progress, don't scroll further yet. // We wait for scrollHeight to change or the stuck detection to trigger a refresh. if (isTemporarilyStuckLoading) { fluidAnimationFrameId = requestAnimationFrame(animateScroll); // Keep animation loop alive but don't scroll return; } } else { // Scroll progress made clearTimeout(noScrollProgressTimeout); noScrollProgressTimeout = null; // If we were temporarily stuck, reset to original speed if (isTemporarilyStuckLoading) { isTemporarilyStuckLoading = false; settings.pixelsPerFrame = originalPixelsPerFrame; updateStatus("Resuming normal scroll speed...", 'var(--primary-color, #4CAF50)'); } } // Only scroll if not in a temporarily stuck loading state if (!isTemporarilyStuckLoading) { window.scrollBy(0, settings.pixelsPerFrame); // Use current (potentially modified) setting } lastScrollY = currentScrollY; lastScrollHeight = currentScrollHeight; fluidAnimationFrameId = requestAnimationFrame(animateScroll); } fluidAnimationFrameId = requestAnimationFrame(animateScroll); updateStatus("Scrolling feed fluidly...", 'var(--primary-color, #4CAF50)'); } // Stops fluid scrolling function stopFluidScrolling() { if (fluidAnimationFrameId) { cancelAnimationFrame(fluidAnimationFrameId); fluidAnimationFrameId = null; } clearTimeout(noScrollProgressTimeout); // Clear loading timeout if stopping noScrollProgressTimeout = null; if (isTemporarilyStuckLoading) { // Reset speed if it was slowed down settings.pixelsPerFrame = originalPixelsPerFrame; isTemporarilyStuckLoading = false; } } // Starts jumpy scrolling using setInterval (for background tabs) function startJumpyScrolling() { if (jumpyScrollIntervalId) return; // Already jumpy scrolling // Ensure fluid scrolling is stopped if (fluidAnimationFrameId) { cancelAnimationFrame(fluidAnimationFrameId); fluidAnimationFrameId = null; } jumpyScrollIntervalId = setInterval(() => { if (!scrolling) { clearInterval(jumpyScrollIntervalId); jumpyScrollIntervalId = null; return; } const currentScrollHeight = document.documentElement.scrollHeight; if (currentScrollHeight === lastScrollHeight) { // No scroll progress if (!noScrollProgressTimeout) { noScrollProgressTimeout = setTimeout(() => { if (document.documentElement.scrollHeight === lastScrollHeight) { isTemporarilyStuckLoading = true; settings.jumpyScrollInterval = Math.round(originalJumpyScrollInterval * SLOW_SCROLL_FACTOR_JUMPY); updateStatus(`Slowing for loading (${settings.jumpyScrollInterval}ms interval)...`, 'orange'); // To apply new interval, restart the interval clearInterval(jumpyScrollIntervalId); jumpyScrollIntervalId = null; startJumpyScrolling(); // Call itself to restart with new interval } noScrollProgressTimeout = null; }, NO_SCROLL_PROGRESS_WARN_DELAY); } // If we are in a slowed-down state, or just detected no scroll progress, don't jump further yet. if (isTemporarilyStuckLoading) { // Do nothing, the interval will just keep firing at the new (slower) rate, waiting for scrollHeight to change. return; } } else { // Scroll progress made clearTimeout(noScrollProgressTimeout); noScrollProgressTimeout = null; // If we were temporarily stuck, reset to original speed if (isTemporarilyStuckLoading) { isTemporarilyStuckLoading = false; settings.jumpyScrollInterval = originalJumpyScrollInterval; updateStatus("Resuming normal scroll speed...", 'var(--primary-color, #4CAF50)'); // To apply new interval, restart the interval clearInterval(jumpyScrollIntervalId); jumpyScrollIntervalId = null; startJumpyScrolling(); // Call itself to restart with original interval } } // Only scroll if not in a temporarily stuck loading state if (!isTemporarilyStuckLoading) { window.scrollTo({ top: currentScrollHeight, behavior: 'instant' }); } lastScrollHeight = currentScrollHeight; }, settings.jumpyScrollInterval); // Use current (potentially modified) setting updateStatus("Scrolling feed (jumpy in background)...", 'var(--primary-color, #4CAF50)'); } // Stops jumpy scrolling function stopJumpyScrolling() { if (jumpyScrollIntervalId) { clearInterval(jumpyScrollIntervalId); jumpyScrollIntervalId = null; } clearTimeout(noScrollProgressTimeout); // Clear loading timeout if stopping noScrollProgressTimeout = null; if (isTemporarilyStuckLoading) { // Reset speed if it was slowed down settings.jumpyScrollInterval = originalJumpyScrollInterval; isTemporarilyStuckLoading = false; } } // --- Stuck Detection Logic (for full page refresh - ultimate fallback) --- let lastKnownScrollYForStuckDetection = 0; let lastKnownScrollHeightForStuckDetection = 0; function startStuckDetection() { if (stuckDetectionIntervalId) clearInterval(stuckDetectionIntervalId); // Clear any existing interval // Initialize last known scroll positions and time when detection starts lastKnownScrollYForStuckDetection = window.scrollY; lastKnownScrollHeightForStuckDetection = document.documentElement.scrollHeight; lastEffectiveScrollTime = Date.now(); // Reset time when detection starts stuckDetectionIntervalId = setInterval(() => { if (!scrolling) { // Only check if the bot is actively running clearInterval(stuckDetectionIntervalId); stuckDetectionIntervalId = null; return; } const currentScrollY = window.scrollY; const currentScrollHeight = document.documentElement.scrollHeight; // Check if *any* scroll progress has been made since the last check by THIS interval if (currentScrollY !== lastKnownScrollYForStuckDetection || currentScrollHeight !== lastKnownScrollHeightForStuckDetection) { lastEffectiveScrollTime = Date.now(); // Scroll position has changed, reset the "stuck" timer } // If no effective scroll progress for STUCK_TIMEOUT_MS, consider it truly stuck and reload if (Date.now() - lastEffectiveScrollTime > STUCK_TIMEOUT_MS) { console.warn(`[Instagram Bot] Bot appears truly stuck (no scroll progress for ${STUCK_TIMEOUT_MS / 1000}s). Initiating page refresh.`); updateStatus("Bot stuck! Refreshing page...", 'red'); // Use red for critical error/refresh stopBot(); // Stop cleanly before forcing a reload localStorage.setItem(AUTO_START_FLAG, 'true'); // Set flag to auto-start after reload location.reload(); // Reload the page } // Update for the next check by this interval lastKnownScrollYForStuckDetection = currentScrollY; lastKnownScrollHeightForStuckDetection = currentScrollHeight; }, 5000); // Check every 5 seconds for stuck state } // Main function to start the bot function startBot() { if (scrolling) return; // Bot is already active scrolling = true; // Ensure original speeds are correct before starting originalPixelsPerFrame = settings.pixelsPerFrame; originalJumpyScrollInterval = settings.jumpyScrollInterval; isTemporarilyStuckLoading = false; // Reset loading state // Start the post processing interval, which runs regardless of scroll mode if (!processPostsIntervalId) { processPostsIntervalId = setInterval(async () => { await processPostsBatch(); }, processPostsInterval); } // Determine initial scrolling mode based on visibility if (document.visibilityState === 'visible') { startFluidScrolling(); } else { startJumpyScrolling(); } // Start monitoring for stuck state when the bot starts startStuckDetection(); } // Main function to stop the bot function stopBot() { if (!scrolling) return; // Bot is already stopped scrolling = false; stopFluidScrolling(); // Stop fluid scrolling if active stopJumpyScrolling(); // Stop jumpy scrolling if active // Stop the post processing interval if (processPostsIntervalId) { clearInterval(processPostsIntervalId); processPostsIntervalId = null; } // Stop the stuck detection interval if (stuckDetectionIntervalId) { clearInterval(stuckDetectionIntervalId); stuckDetectionIntervalId = null; } clearTimeout(noScrollProgressTimeout); // Clear any pending no-scroll checks noScrollProgressTimeout = null; updateStatus("Stopped.", 'var(--warning-color, #f44336)'); } // --- Event Listener for Tab Visibility Change --- document.addEventListener('visibilitychange', () => { if (scrolling) { // Only change mode if bot is currently active // Stop current scrolling mode cleanly stopFluidScrolling(); stopJumpyScrolling(); // Restart scrolling in the appropriate new mode if (document.visibilityState === 'visible') { startFluidScrolling(); } else { startJumpyScrolling(); } // The stuck detection interval continues running, as it monitors overall scroll progress } }); // --- Custom Confirmation Modal Function --- function showCustomConfirmation(postElement, postSnippet, isSponsored, targetLinkHref) { console.log("Attempting to show custom confirmation modal."); return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'tm-custom-modal-overlay'; Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '100000', display: 'flex', justifyContent: 'center', alignItems: 'center', opacity: '1', // Set opacity directly }); const modal = document.createElement('div'); modal.id = 'tm-custom-modal'; Object.assign(modal.style, { backgroundColor: '#262626', color: '#F0F0F0', borderRadius: '12px', padding: '25px', boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)', maxWidth: '450px', width: '90%', fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif', display: 'flex', flexDirection: 'column', gap: '15px', transform: 'scale(1)', // Set transform directly }); const header = document.createElement('div'); Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '10px', marginBottom: '10px' }); const title = document.createElement('h3'); title.innerText = 'Potential Offer Found!'; Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: 'bold', color: '#66B3FF' }); const closeBtn = document.createElement('button'); closeBtn.innerText = '✕'; Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#F0F0F0', fontSize: '20px', cursor: 'pointer', padding: '5px', lineHeight: '1' }); closeBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); }; header.appendChild(title); header.appendChild(closeBtn); modal.appendChild(header); const body = document.createElement('div'); Object.assign(body.style, { fontSize: '15px', lineHeight: '1.6', color: '#E0E0E0' }); const infoText = document.createElement('p'); infoText.innerHTML = `An offer has been detected based on your criteria.` + (isSponsored ? ` <span style="font-weight: bold; color: #FFEB3B;">(Sponsored Post)</span>` : ''); Object.assign(infoText.style, { margin: '0 0 10px 0' }); const snippetHeader = document.createElement('p'); snippetHeader.innerText = 'Post Snippet:'; Object.assign(snippetHeader.style, { margin: '0', fontWeight: 'bold', color: '#B3B3FF' }); const snippetContent = document.createElement('code'); snippetContent.innerText = postSnippet; Object.assign(snippetContent.style, { display: 'block', backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: '5px', padding: '10px', fontSize: '13px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: '100px', overflowY: 'auto', marginTop: '5px', marginBottom: '10px', color: '#C0C0C0' }); body.appendChild(infoText); body.appendChild(snippetHeader); body.appendChild(snippetContent); if (targetLinkHref) { const linkP = document.createElement('p'); linkP.innerText = 'Detected Link:'; Object.assign(linkP.style, { margin: '0', fontWeight: 'bold', color: '#B3B3FF' }); const linkElement = document.createElement('a'); linkElement.href = targetLinkHref; linkElement.target = '_blank'; linkElement.innerText = targetLinkHref.length > 60 ? targetLinkHref.substring(0, 57) + '...' : targetLinkHref; Object.assign(linkElement.style, { display: 'block', marginTop: '5px', color: '#66B3FF', wordBreak: 'break-all', textDecoration: 'underline', fontSize: '13px' }); // Note: The click handler for this link element itself should not resolve the promise, // as the user might click it and then still want to dismiss the modal. body.appendChild(linkP); body.appendChild(linkElement); } else { const noLinkP = document.createElement('p'); noLinkP.innerText = "No direct link found. You'll need to manually check this post."; Object.assign(noLinkP.style, { margin: '0', fontStyle: 'italic', color: '#A0A0A0' }); body.appendChild(noLinkP); } modal.appendChild(body); const footer = document.createElement('div'); Object.assign(footer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px', borderTop: '1px solid rgba(255,255,255,0.1)', marginTop: '10px' }); // Button styling template const buttonStyle = { padding: '10px 20px', borderRadius: '8px', border: 'none', fontWeight: 'bold', cursor: 'pointer', transition: 'background-color 0.2s ease, transform 0.1s ease', }; const primaryButtonStyle = { backgroundColor: '#66B3FF', color: 'white', onmouseover: (btn) => btn.style.backgroundColor = '#4DA8FF', onmouseout: (btn) => btn.style.backgroundColor = '#66B3FF', onmousedown: (btn) => btn.style.transform = 'translateY(1px)', onmouseup: (btn) => btn.style.transform = 'translateY(0)', }; const secondaryButtonStyle = { border: '1px solid rgba(255,255,255,0.3)', backgroundColor: 'transparent', color: '#E0E0E0', onmouseover: (btn) => { btn.style.backgroundColor = 'rgba(255,255,255,0.1)'; btn.style.color = '#FFFFFF'; }, onmouseout: (btn) => { btn.style.backgroundColor = 'transparent'; btn.style.color = '#E0E0E0'; }, onmousedown: (btn) => btn.style.transform = 'translateY(1px)', onmouseup: (btn) => btn.style.transform = 'translateY(0)', }; // "Open Link" button (if targetLinkHref exists) if (targetLinkHref) { const openLinkBtn = document.createElement('button'); openLinkBtn.innerText = 'Open Link'; Object.assign(openLinkBtn.style, buttonStyle, primaryButtonStyle); openLinkBtn.onmouseover = () => primaryButtonStyle.onmouseover(openLinkBtn); openLinkBtn.onmouseout = () => primaryButtonStyle.onmouseout(openLinkBtn); openLinkBtn.onmousedown = () => primaryButtonStyle.onmousedown(openLinkBtn); openLinkBtn.onmouseup = () => primaryButtonStyle.onmouseup(openLinkBtn); openLinkBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve('open_link'); // Resolve with 'open_link' }; footer.appendChild(openLinkBtn); } // "Jump to Post" button (always available, but style changes) const jumpToPostBtn = document.createElement('button'); jumpToPostBtn.innerText = 'Jump to Post'; // If there's a direct link, Jump to Post becomes a secondary action. Otherwise, it's primary. const jumpBtnStyle = targetLinkHref ? secondaryButtonStyle : primaryButtonStyle; Object.assign(jumpToPostBtn.style, buttonStyle, jumpBtnStyle); jumpToPostBtn.onmouseover = () => jumpBtnStyle.onmouseover(jumpToPostBtn); jumpToPostBtn.onmouseout = () => jumpBtnStyle.onmouseout(jumpToPostBtn); jumpToPostBtn.onmousedown = () => jumpBtnStyle.onmousedown(jumpToPostBtn); jumpToPostBtn.onmouseup = () => jumpBtnStyle.onmouseup(jumpToPostBtn); jumpToPostBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); if (postElement) { postElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); console.log("Attempted to scroll to post."); } resolve('jump'); // Resolve with 'jump' }; footer.appendChild(jumpToPostBtn); // "Dismiss & Resume" button (always available) const dismissBtn = document.createElement('button'); dismissBtn.innerText = 'Dismiss & Resume'; Object.assign(dismissBtn.style, buttonStyle, secondaryButtonStyle); dismissBtn.onmouseover = () => secondaryButtonStyle.onmouseover(dismissBtn); dismissBtn.onmouseout = () => secondaryButtonStyle.onmouseout(dismissBtn); dismissBtn.onmousedown = () => secondaryButtonStyle.onmousedown(dismissBtn); dismissBtn.onmouseup = () => secondaryButtonStyle.onmouseup(dismissBtn); dismissBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve('dismiss'); }; // Resolve with 'dismiss' footer.appendChild(dismissBtn); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); const handleEscape = (e) => { if (e.key === 'Escape') { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve('dismiss'); // Escape key also dismisses and resumes } }; document.addEventListener('keydown', handleEscape); }); } // --- Help Modal Function --- async function showHelpModal() { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'tm-help-overlay'; Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '100001', display: 'flex', justifyContent: 'center', alignItems: 'center', opacity: '0', transition: 'opacity 0.3s ease-in-out' }); const modal = document.createElement('div'); modal.id = 'tm-help-modal'; Object.assign(modal.style, { backgroundColor: '#262626', color: '#F0F0F0', borderRadius: '12px', padding: '25px', boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)', maxWidth: '450px', width: '90%', fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif', display: 'flex', flexDirection: 'column', gap: '15px', transform: 'scale(0.9)', transition: 'transform 0.3s ease-in-out' }); const header = document.createElement('div'); Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '10px', marginBottom: '10px' }); const title = document.createElement('h3'); title.innerText = 'Bot Help & Troubleshooting'; Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: 'bold', color: '#66B3FF' }); const closeBtn = document.createElement('button'); closeBtn.innerText = '✕'; Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#F0F0F0', fontSize: '20px', cursor: 'pointer', padding: '5px', lineHeight: '1' }); closeBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); }; header.appendChild(title); header.appendChild(closeBtn); modal.appendChild(header); const content = document.createElement('div'); Object.assign(content.style, { fontSize: '14px', lineHeight: '1.6', color: '#E0E0E0', maxHeight: '300px', overflowY: 'auto' }); content.innerHTML = ` <p>If the Instagram Bot isn't working as expected, try these steps:</p> <ul> <li><strong>Ensure Tampermonkey is active:</strong> Check your browser's Tampermonkey extension icon; it should be enabled.</li> <li><strong>Script Enabled:</strong> In the Tampermonkey dashboard, make sure 'Instagram Sample Bot' is toggled ON.</li> <li><strong>Disable Ad Blockers:</strong> Ad blockers (e.g., uBlock Origin, AdBlock Plus) or privacy extensions can interfere. Try disabling them for instagram.com.</li> <li><strong>Refresh Page:</strong> A simple page refresh (F5 or Ctrl+R / Cmd+R) can often resolve minor issues.</li> <li><strong>Restart Bot:</strong> If the bot stops, try clicking the 'Start Bot' button in the UI.</li> <li><strong>Check Console for Errors:</strong> Open your browser's developer tools (F12 or Ctrl+Shift+I / Cmd+Option+I), go to the 'Console' tab, and look for any red error messages. Report them to my discord @dprits419 if you need further help.</li> <li><strong>Update Script:</strong> Click 'Update Userscript' in the settings to check for the latest version.</li> <li><strong>Clear Cache/Cookies:</strong> As a last resort, clearing browser cache and cookies for instagram.com can sometimes fix persistent issues (note: this will log you out).</li> </ul> <p>If you still face problems, please contact my discord @dprits419 with details!</p> `; modal.appendChild(content); const footer = document.createElement('div'); Object.assign(footer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px', borderTop: '1px solid rgba(255,255,255,0.1)', marginTop: '10px' }); const closeButton = document.createElement('button'); closeButton.innerText = 'Close'; Object.assign(closeButton.style, { padding: '10px 20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.3)', backgroundColor: 'transparent', color: '#E0E0E0', cursor: 'pointer', transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease' }); closeButton.onmouseover = () => { closeButton.style.backgroundColor = 'rgba(255,255,255,0.1)'; closeButton.style.color = '#FFFFFF'; }; closeButton.onmouseout = () => { closeButton.style.backgroundColor = 'transparent'; closeButton.style.color = '#E0E0E0'; }; closeButton.onmousedown = () => closeButton.style.transform = 'translateY(1px)'; closeButton.onmouseup = () => closeButton.style.transform = 'translateY(0)'; closeButton.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); }; footer.appendChild(closeButton); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); setTimeout(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1)'; }, 10); const handleEscape = (e) => { if (e.key === 'Escape') { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); } }; document.addEventListener('keydown', handleEscape); }); } // --- Settings Modal Function --- async function showSettingsModal() { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'tm-settings-overlay'; Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '100001', display: 'flex', justifyContent: 'center', alignItems: 'center', opacity: '0', transition: 'opacity 0.3s ease-in-out' }); const modal = document.createElement('div'); modal.id = 'tm-settings-modal'; Object.assign(modal.style, { backgroundColor: '#262626', color: '#F0F0F0', borderRadius: '12px', padding: '25px', boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)', maxWidth: '400px', width: '90%', fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif', display: 'flex', flexDirection: 'column', gap: '15px', transform: 'scale(0.9)', transition: 'transform 0.3s ease-in-out' }); const header = document.createElement('div'); Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '10px', marginBottom: '10px' }); const title = document.createElement('h3'); title.innerText = 'Bot Settings'; Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: 'bold', color: '#66B3FF' }); const closeBtn = document.createElement('button'); closeBtn.innerText = '✕'; Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#F0F0F0', fontSize: '20px', cursor: 'pointer', padding: '5px', lineHeight: '1' }); closeBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); }; header.appendChild(title); header.appendChild(closeBtn); modal.appendChild(header); const settingsForm = document.createElement('div'); Object.assign(settingsForm.style, { display: 'flex', flexDirection: 'column', gap: '12px' }); // Helper to create a setting row const createSettingRow = (labelText, inputElement) => { const row = document.createElement('div'); Object.assign(row.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }); const label = document.createElement('label'); label.innerText = labelText; Object.assign(label.style, { flexShrink: '0', marginRight: '10px', fontSize: '14px', color: '#E0E0E0' }); row.appendChild(label); row.appendChild(inputElement); return row; }; // Scrolling Speed (Fluid) const fluidSpeedInput = document.createElement('input'); fluidSpeedInput.type = 'number'; fluidSpeedInput.min = '1'; fluidSpeedInput.max = '20'; fluidSpeedInput.step = '1'; fluidSpeedInput.value = settings.pixelsPerFrame; Object.assign(fluidSpeedInput.style, { width: '80px', padding: '8px', borderRadius: '5px', border: '1px solid #555', backgroundColor: '#333', color: '#F0F0F0', fontSize: '14px' }); settingsForm.appendChild(createSettingRow('Fluid Scroll Speed (px/frame):', fluidSpeedInput)); // Scrolling Interval (Jumpy) const jumpyIntervalInput = document.createElement('input'); jumpyIntervalInput.type = 'number'; jumpyIntervalInput.min = '100'; jumpyIntervalInput.max = '5000'; jumpyIntervalInput.step = '100'; jumpyIntervalInput.value = settings.jumpyScrollInterval; Object.assign(jumpyIntervalInput.style, { width: '80px', padding: '8px', borderRadius: '5px', border: '1px solid #555', backgroundColor: '#333', color: '#F0F0F0', fontSize: '14px' }); settingsForm.appendChild(createSettingRow('Jumpy Scroll Interval (ms):', jumpyIntervalInput)); // Auto-Liking Toggle const autoLikeToggle = document.createElement('input'); autoLikeToggle.type = 'checkbox'; autoLikeToggle.checked = settings.enableLiking; Object.assign(autoLikeToggle.style, { width: '20px', height: '20px', cursor: 'pointer' }); settingsForm.appendChild(createSettingRow('Enable Auto-Liking:', autoLikeToggle)); // Sample Detection Toggle const sampleDetectToggle = document.createElement('input'); sampleDetectToggle.type = 'checkbox'; sampleDetectToggle.checked = settings.enableSampleDetection; Object.assign(sampleDetectToggle.style, { width: '20px', height: '20px', cursor: 'pointer' }); settingsForm.appendChild(createSettingRow('Enable Sample Detection:', sampleDetectToggle)); // Confidence Threshold for Sample Detection const confidenceThresholdInput = document.createElement('input'); confidenceThresholdInput.type = 'number'; confidenceThresholdInput.min = '0.0'; confidenceThresholdInput.max = '5.0'; confidenceThresholdInput.step = '0.1'; // Max increased to accommodate new scoring confidenceThresholdInput.value = settings.minConfidenceThreshold; Object.assign(confidenceThresholdInput.style, { width: '80px', padding: '8px', borderRadius: '5px', border: '1px solid #555', backgroundColor: '#333', color: '#F0F0F0', fontSize: '14px' }); settingsForm.appendChild(createSettingRow('Sample Confidence Threshold:', confidenceThresholdInput)); // Accept Only Popular Brands Toggle const popularBrandsToggle = document.createElement('input'); popularBrandsToggle.type = 'checkbox'; popularBrandsToggle.checked = settings.acceptOnlyPopularBrands; Object.assign(popularBrandsToggle.style, { width: '20px', height: '20px', cursor: 'pointer' }); settingsForm.appendChild(createSettingRow('Only Popular Beauty Brands:', popularBrandsToggle)); modal.appendChild(settingsForm); const footer = document.createElement('div'); Object.assign(footer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px', borderTop: '1px solid rgba(255,255,255,0.1)', marginTop: '10px', flexWrap: 'wrap' }); // Added flexWrap // UPDATED: Help Button now opens in-app modal const helpBtn = document.createElement('button'); helpBtn.innerText = 'Help'; Object.assign(helpBtn.style, { padding: '10px 15px', borderRadius: '8px', border: '1px solid rgba(102, 179, 255, 0.5)', backgroundColor: 'transparent', color: '#66B3FF', fontWeight: 'bold', cursor: 'pointer', transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease', flexShrink: '0' // Prevent shrinking }); helpBtn.onmouseover = () => { helpBtn.style.backgroundColor = 'rgba(102, 179, 255, 0.1)'; }; helpBtn.onmouseout = () => { helpBtn.style.backgroundColor = 'transparent'; }; helpBtn.onmousedown = () => helpBtn.style.transform = 'translateY(1px)'; helpBtn.onmouseup = () => helpBtn.style.transform = 'translateY(0)'; helpBtn.onclick = async () => { console.log("Help button clicked from settings modal."); try { // Do NOT close the settings modal here. Just show the help modal. await showHelpModal(); } catch (e) { console.error("Error executing showHelpModal from settings:", e); // No status update here, as the settings modal is still open } }; footer.appendChild(helpBtn); // Update Userscript Button const updateBtn = document.createElement('button'); updateBtn.innerText = 'Update Userscript'; Object.assign(updateBtn.style, { padding: '10px 15px', borderRadius: '8px', border: '1px solid rgba(144, 238, 144, 0.5)', backgroundColor: 'transparent', color: '#90EE90', fontWeight: 'bold', cursor: 'pointer', transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease', flexShrink: '0' // Prevent shrinking }); updateBtn.onmouseover = () => { updateBtn.style.backgroundColor = 'rgba(144, 238, 144, 0.1)'; }; updateBtn.onmouseout = () => { updateBtn.style.backgroundColor = 'transparent'; }; updateBtn.onmousedown = () => updateBtn.style.transform = 'translateY(1px)'; updateBtn.onmouseup = () => updateBtn.style.transform = 'translateY(0)'; updateBtn.onclick = () => { window.open(SCRIPT_INSTALL_URL, '_blank'); }; footer.appendChild(updateBtn); const saveBtn = document.createElement('button'); saveBtn.innerText = 'Save Settings'; Object.assign(saveBtn.style, { padding: '10px 20px', borderRadius: '8px', border: 'none', backgroundColor: '#66B3FF', color: 'white', fontWeight: 'bold', cursor: 'pointer', transition: 'background-color 0.2s ease, transform 0.1s ease', flexShrink: '0' }); saveBtn.onmouseover = () => saveBtn.style.backgroundColor = '#4DA8FF'; saveBtn.onmouseout = () => saveBtn.style.backgroundColor = '#66B3FF'; saveBtn.onmousedown = () => saveBtn.style.transform = 'translateY(1px)'; saveBtn.onmouseup = () => saveBtn.style.transform = 'translateY(0)'; saveBtn.onclick = () => { settings.pixelsPerFrame = parseInt(fluidSpeedInput.value); settings.jumpyScrollInterval = parseInt(jumpyIntervalInput.value); settings.enableLiking = autoLikeToggle.checked; settings.enableSampleDetection = sampleDetectToggle.checked; settings.minConfidenceThreshold = parseFloat(confidenceThresholdInput.value); settings.acceptOnlyPopularBrands = popularBrandsToggle.checked; saveSettings(); document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(true); // Indicate settings were saved }; footer.appendChild(saveBtn); const cancelBtn = document.createElement('button'); cancelBtn.innerText = 'Cancel'; Object.assign(cancelBtn.style, { padding: '10px 20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.3)', backgroundColor: 'transparent', color: '#E0E0E0', cursor: 'pointer', transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease' }); cancelBtn.onmouseover = () => { cancelBtn.style.backgroundColor = 'rgba(255,255,255,0.1)'; cancelBtn.style.color = '#FFFFFF'; }; cancelBtn.onmouseout = () => { cancelBtn.style.backgroundColor = 'transparent'; cancelBtn.style.color = '#E0E0E0'; }; cancelBtn.onmousedown = () => cancelBtn.style.transform = 'translateY(1px)'; cancelBtn.onmouseup = () => cancelBtn.style.transform = 'translateY(0)'; cancelBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); }; footer.appendChild(cancelBtn); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); setTimeout(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1)'; }, 10); const handleEscape = (e) => { if (e.key === 'Escape') { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); } }; document.addEventListener('keydown', handleEscape); }); } async function processPostsBatch() { const posts = document.querySelectorAll('article'); for (const post of posts) { const postText = post.innerText.toLowerCase(); // --- LIKING LOGIC --- if (settings.enableLiking && !post.dataset.likedChecked) { post.dataset.likedChecked = 'true'; const shouldLike = cologneKeywords.some(kw => postText.includes(kw)) || beautyKeywords.some(kw => postText.includes(kw)); if (shouldLike) { const likedSuccessfully = await likePost(post); if (likedSuccessfully) { updateStatus("Liked a post!", 'var(--primary-color)'); await delay(1000 + Math.random() * 1000); // Wait 1-2 seconds after liking } } } // --- SAMPLE DETECTION LOGIC --- if (!settings.enableSampleDetection || post.dataset.sampleChecked) { continue; } // Filter by popular brands if setting is enabled if (settings.acceptOnlyPopularBrands) { const foundPopularBrand = popularBeautyBrands.some(brand => postText.includes(brand.toLowerCase())); if (!foundPopularBrand) { continue; // Skip this post if it's not from a popular brand } } let confidence = 0; let targetLinkHref = null; // Changed to store href directly const isSponsored = isPostSponsored(post); let hasAnySampleKeyword = false; let hasNegativePhrase = false; // 1. Boost for strong offer phrases (highest priority) for (const phrase of strongOfferPhrases) { if (postText.includes(phrase)) { confidence += 3.0; // High boost for direct offer phrases } } // 2. Boost for action texts (strong indicator of an interactive offer) const buttonsAndLinks = post.querySelectorAll('a, button'); for (const element of buttonsAndLinks) { const elementText = element.innerText.toLowerCase(); if (sampleActionTexts.some(action => elementText.includes(action))) { if (element.tagName === 'A' && element.href && element.href !== '#' && !element.href.startsWith('javascript:')) { // Prioritize action-associated links if found, or if no direct text link yet if (!targetLinkHref || element.href.includes('bit.ly') || element.href.includes('tinyurl')) { // Prioritize short links or if no other link targetLinkHref = element.href; } } confidence += 2.0; // Strong boost for action buttons/links break; // Only need to find one action text } } // NEW: Extract direct links from post text (more robust URL detection) const urlRegex = /(https?:\/\/[^\s]+)/g; // Basic URL regex const matches = postText.match(urlRegex); if (matches && matches.length > 0) { // If we found a direct URL in the text, use it if targetLinkHref isn't already set by an action button if (!targetLinkHref) { targetLinkHref = matches[0]; // Take the first URL found } confidence += 1.0; // Add confidence for finding a direct link } // 3. Boost for sponsored posts if (isSponsored) { confidence += 1.0; // Bonus for sponsored posts } // Identify presence of any generic sample keyword for (const keyword of sampleKeywords) { if (postText.includes(keyword)) { hasAnySampleKeyword = true; break; } } // Identify presence of any negative sample phrase for (const negPhrase of negativeSamplePhrases) { if (postText.includes(negPhrase)) { hasNegativePhrase = true; break; } } // 4. Conditional boost/penalty for general sample keywords based on negative context if (hasAnySampleKeyword) { if (!hasNegativePhrase) { confidence += 1.5; // Significant boost if general keyword is clean } else { confidence -= 2.5; // Strong penalty if a negative context is present } } // Ensure confidence doesn't go below zero after penalties confidence = Math.max(0, confidence); if (confidence >= settings.minConfidenceThreshold) { updateStatus("Potential offer found! Waiting for your decision...", 'var(--highlight-color, #FFD700)'); // Stop all bot activity (scrolling and processing) stopBot(); post.dataset.sampleChecked = 'true'; const postSnippet = postText.substring(0, Math.min(postText.length, 300)); // targetLinkHref is already determined above // Pass the actual post element to the confirmation modal const userChoice = await showCustomConfirmation(post, postSnippet, isSponsored, targetLinkHref); if (userChoice === 'open_link') { // User chose to open the link if (targetLinkHref) { // Double check link exists before opening window.open(targetLinkHref, '_blank'); updateStatus("Opened link. Resuming bot...", 'var(--primary-color, #4CAF50)'); } else { updateStatus("No link to open. Resuming bot...", 'var(--warning-color, #FFA500)'); } // Automatically restart the bot after user interaction setTimeout(() => { startBot(); }, 0); } else if (userChoice === 'jump') { // User chose to jump to the post updateStatus("Jumped to post. Bot stopped for inspection.", 'var(--warning-color, #FFA500)'); // The bot remains stopped here. No startBot() call. } else { // userChoice === 'dismiss' (User chose to dismiss and resume) updateStatus("User chose to dismiss. Resuming bot...", 'var(--primary-color, #4CAF50)'); // Automatically restart the bot after user interaction setTimeout(() => { startBot(); }, 0); } return; // Stop processing other posts in this batch to avoid immediate re-trigger } } } // --- UI Creation --- function createUI() { const rootStyle = document.documentElement.style; rootStyle.setProperty('--primary-color', '#4CAF50'); rootStyle.setProperty('--danger-color', '#f44336'); rootStyle.setProperty('--text-color', '#333'); rootStyle.setProperty('--bg-color', 'rgba(255, 255, 255, 0.7)'); rootStyle.setProperty('--border-color', 'rgba(200, 200, 200, 0.5)'); rootStyle.setProperty('--shadow-color', 'rgba(0, 0, 0, 0.1)'); rootStyle.setProperty('--highlight-color', '#FFD700'); rootStyle.setProperty('--warning-color', '#FFA500'); rootStyle.setProperty('--settings-icon-color', '#8A2BE2'); // A vibrant blue-violet for contrast const uiContainer = document.createElement('div'); Object.assign(uiContainer.style, { position: 'fixed', top: '20px', left: '20px', backgroundColor: 'var(--bg-color)', backdropFilter: 'blur(8px)', padding: '15px', border: '1px solid var(--border-color)', borderRadius: '12px', boxShadow: '0 4px 15px var(--shadow-color)', zIndex: '99999', fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif', display: 'flex', flexDirection: 'column', gap: '15px', alignItems: 'center', justifyContent: 'center', minWidth: '240px', // Initial state for fluid entry opacity: '0', transform: 'translateY(-20px)', transition: 'opacity 0.5s ease-out, transform 0.5s ease-out', // Draggable styles cursor: 'grab', // Indicate it's draggable userSelect: 'none', // Prevent text selection during drag }); // --- Draggable UI Logic --- let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; uiContainer.addEventListener("mousedown", dragStart); document.addEventListener("mouseup", dragEnd); document.addEventListener("mousemove", drag); // Listen on document for smoother dragging outside element function dragStart(e) { initialX = e.clientX - uiContainer.getBoundingClientRect().left; // Use getBoundingClientRect for initial position initialY = e.clientY - uiContainer.getBoundingClientRect().top; // Use getBoundingClientRect for initial position if (e.target === uiContainer || uiContainer.contains(e.target)) { // Only drag if click is on container or its children isDragging = true; uiContainer.style.cursor = 'grabbing'; // Change cursor to indicate dragging } } function dragEnd(e) { isDragging = false; uiContainer.style.cursor = 'grab'; // Reset cursor } function drag(e) { if (isDragging) { e.preventDefault(); // Prevent default browser drag behavior // Calculate new position let newX = e.clientX - initialX; let newY = e.clientY - initialY; // Get viewport dimensions const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Get UI container dimensions const uiWidth = uiContainer.offsetWidth; const uiHeight = uiContainer.offsetHeight; // Clamp X position within viewport newX = Math.max(0, Math.min(newX, viewportWidth - uiWidth)); // Clamp Y position within viewport newY = Math.max(0, Math.min(newY, viewportHeight - uiHeight)); // Apply new position uiContainer.style.left = newX + "px"; uiContainer.style.top = newY + "px"; // Update offsets for next drag event xOffset = newX; yOffset = newY; } } // --- End Draggable UI Logic --- const headerRow = document.createElement('div'); Object.assign(headerRow.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', }); uiContainer.appendChild(headerRow); const titleElement = document.createElement('h3'); titleElement.innerText = `Instagram Bot v${SCRIPT_VERSION}`; Object.assign(titleElement.style, { margin: '0', color: 'var(--text-color)', fontSize: '20px', fontWeight: 'bold', flexGrow: '1', textAlign: 'center' }); headerRow.appendChild(titleElement); // Settings Button const settingsButton = document.createElement('button'); settingsButton.innerHTML = '⚙️'; Object.assign(settingsButton.style, { background: 'none', border: 'none', fontSize: '28px', cursor: 'pointer', color: 'var(--settings-icon-color)', padding: '0', lineHeight: '1', transition: 'transform 0.1s ease, color 0.2s ease', }); settingsButton.onmouseover = () => settingsButton.style.transform = 'rotate(20deg)'; settingsButton.onmouseout = () => settingsButton.style.transform = 'rotate(0deg)'; settingsButton.onclick = async () => { console.log("Settings button clicked."); try { await showSettingsModal(); } catch (e) { console.error("Error executing showSettingsModal:", e); updateStatus("Error opening settings!", 'red'); } }; headerRow.appendChild(settingsButton); const buttonsContainer = document.createElement('div'); Object.assign(buttonsContainer.style, { display: 'flex', gap: '12px', width: '100%', justifyContent: 'center', }); uiContainer.appendChild(buttonsContainer); const buttonStyle = { padding: '10px 18px', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer', transition: 'background-color 0.2s ease, transform 0.1s ease', boxShadow: '0 2px 5px var(--shadow-color)', flexGrow: '1', }; const startButton = document.createElement('button'); startButton.innerText = 'Start Bot'; Object.assign(startButton.style, buttonStyle, { backgroundColor: 'var(--primary-color)', color: 'white' }); startButton.onmouseover = () => startButton.style.backgroundColor = '#45a049'; startButton.onmouseout = () => startButton.style.backgroundColor = 'var(--primary-color)'; startButton.onmousedown = () => startButton.style.transform = 'translateY(1px)'; startButton.onmouseup = () => startButton.style.transform = 'translateY(0)'; startButton.onclick = () => { console.log("Start Bot button clicked."); try { startBot(); } catch (e) { console.error("Error executing startBot:", e); updateStatus("Error starting bot!", 'red'); } }; buttonsContainer.appendChild(startButton); const stopButton = document.createElement('button'); stopButton.innerText = 'Stop Bot'; Object.assign(stopButton.style, buttonStyle, { backgroundColor: 'var(--danger-color)', color: 'white' }); stopButton.onmouseover = () => stopButton.style.backgroundColor = '#da190b'; stopButton.onmouseout = () => stopButton.style.backgroundColor = 'var(--danger-color)'; stopButton.onmousedown = () => stopButton.style.transform = 'translateY(1px)'; stopButton.onmouseup = () => stopButton.style.transform = 'translateY(0)'; stopButton.onclick = () => { console.log("Stop Bot button clicked."); try { stopBot(); } catch (e) { console.error("Error executing stopBot:", e); updateStatus("Error stopping bot!", 'red'); } }; buttonsContainer.appendChild(stopButton); statusBarElement = document.createElement('div'); Object.assign(statusBarElement.style, { marginTop: '0px', fontSize: '12px', color: 'var(--text-color)', textAlign: 'center', width: '100%', paddingTop: '0px', }); uiContainer.appendChild(statusBarElement); document.body.appendChild(uiContainer); // Animate the UI container into view requestAnimationFrame(() => { uiContainer.style.opacity = '1'; uiContainer.style.transform = 'translateY(0)'; }); updateStatus("Ready for use. Click 'Start Bot'!", 'var(--text-color)'); } // --- Initialization --- window.addEventListener('load', () => { loadSettings(); // Load settings first createUI(); // Then create the UI // Check if we should auto-start the bot after a previous forced reload (due to being stuck) if (localStorage.getItem(AUTO_START_FLAG) === 'true') { localStorage.removeItem(AUTO_START_FLAG); // Clear the flag immediately updateStatus("Restarting after recovery refresh...", 'green'); // Inform user // Give the page a moment to fully render before restarting the bot setTimeout(() => { startBot(); }, 2000); // 2-second delay before auto-starting } }); })();