NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Bing Rewards
// @version V2.1.1
// @description Automatically complete the Microsoft Rewards daily search task.
// @author Amit
// @match https://www.bing.com/*
// @match https://rewards.bing.com/*
// @license MIT
// @icon https://www.bing.com/favicon.ico
// @connect feeds.bbci.co.uk
// @run-at document-end
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant window.close
// @copyright 2026, amit (https://openuserjs.org/users/amit)
// ==/UserScript==
/*=============================================*\
|* CONFIGURATION *|
\*=============================================*/
// Constants
var TIMEOUT_RANGE = GM_getValue("timeout-range", [5, 10]); // Randomize the time to wait between searches
var COOLDOWN_TIMEOUT = GM_getValue("cooldown-timeout", 15); // Cooldown_Timeout between searches
var UNDER_COOLDOWN = GM_getValue("under-cooldown", false); // Workaround for cooldown restriction
var OPEN_RANDOM_LINKS = GM_getValue("open-random-links", true); // Simulate real human searcg by opening links
var QUEUE_OVERLAY_OPEN = GM_getValue("queue-overlay-open", false); // Persist queue overlay open state
// Auto-close-tabs is read dynamically via getAutoCloseTabs() so settings changes take effect without reload.
function getAutoCloseTabs() { return GM_getValue("auto-close-tabs", true); }
// Blacklist: any reward card whose title STARTS WITH one of these strings is skipped
// during collection (case-insensitive). Use for noisy promo cards that we never want to
// auto-click — e.g. "No joke: Get 100 points/day" which has its own bonus mechanics.
const TASK_TITLE_BLACKLIST = [
"No joke",
];
function isTaskBlacklisted(title) {
if (!title) return false;
const lower = title.toLowerCase();
return TASK_TITLE_BLACKLIST.some(prefix => lower.startsWith(prefix.toLowerCase()));
}
console.log({TIMEOUT_RANGE, COOLDOWN_TIMEOUT, UNDER_COOLDOWN, OPEN_RANDOM_LINKS, AUTO_CLOSE_TABS: getAutoCloseTabs()});
var TIMEOUT = (Math.floor(Math.random() * (TIMEOUT_RANGE[1] - TIMEOUT_RANGE[0]) * 1000) + TIMEOUT_RANGE[0] * 1000); // Randomize the timeout with given range
/**
* Generate a random timeout within the configured TIMEOUT_RANGE
* @returns {number} Random timeout in milliseconds
*/
function getRandomTimeout() {
return Math.floor(Math.random() * (TIMEOUT_RANGE[1] - TIMEOUT_RANGE[0]) * 1000) + TIMEOUT_RANGE[0] * 1000;
}
class ExploreOnBing{
keywordsMap = {
flightSearch: ['getaway', 'flight', 'vacation', 'travel', 'trip', 'affordable flight'],
latestNews: ['news', 'what\'s new', 'latest', 'breaking'],
jobSearch: ['job', 'roles', 'career', 'hiring', 'employment'],
bookDeals: ['book', 'read', 'deals', 'reviews', 'reading'],
craftSupplies: ['creative', 'DIY', 'craft', 'supplies', 'kits'],
shoppingDeals: ['deals', 'shopping', 'items', 'buy', 'purchase', 'shopping list'],
insurancePlans: ['insurance', 'protect', 'coverage', 'plans'],
carDeals: ['car', 'vehicle', 'auto', 'road', 'automotive', 'drive'],
creditCards: ['credit card', 'swipe', 'rewards', 'rates'],
internetPlans: ['internet', 'broadband', 'wifi'],
homeUpgrades: ['home', 'upgrade', 'space', 'improvement', 'renovation'],
flowerDelivery: ['flower', 'delivery', 'smile', 'bouquet'],
concertTickets: ['concert', 'show', 'tickets', 'music', 'event'],
cruiseDeals: ['cruise', 'sail', 'destinations', 'vacation'],
weatherInCity: ['weather', 'temperature', 'climate', 'forecast', 'upcoming weather'],
wordMeaning: ['meaning', 'definition', 'explain'],
localRestaurants: ['restaurant', 'food', 'dining', 'eat'],
loanDeals: ['loan', 'rates', 'mortgage', 'financing', 'future', 'personal', 'student loan'],
movie: ['movie', 'film', 'cinema', 'watch'],
sportsScores: ['sports', 'scores', 'match', 'team', 'won', 'league', 'standings'],
lyricsSearch: ['lyrics', 'song', 'music'],
famousPerson: ['person', 'biography', 'who is', 'facts'],
famousQuote: ['quote', 'saying', 'wisdom', 'inspiration'],
stockPrice: ['stock', 'price', 'share', 'market', 'economy', 'stock price'],
eventsNearMe: ['events', 'near me', 'local', 'happening'],
hotelInCity: ['hotel', 'stay', 'accommodation', 'resort'],
healthSymptom: ['health', 'symptom', 'medical', 'treatment'],
relalEstateNearMe: ['real estate', 'house', 'apartment', 'property'],
currencyConversion: ['currency', 'conversion', 'exchange'],
recipeSearch: ['recipe', 'cook', 'bake', 'food'],
gamingSearch: ['gaming', 'game', 'video game', 'console', 'controller', 'xbox', 'playstation'],
streamingSearch: ['streaming', 'stream', 'platforms', 'bundles', 'tv show', 'series', 'streaming platform'],
phonePlans: ['cell phone', 'phone plan', 'mobile', 'talk', 'text', 'carrier', 'cell plan'],
bankingSearch: ['bank', 'banking', 'checking', 'savings account', 'savings', 'account', 'financial'],
locationExplore: ['spot', 'adventure', 'explore', 'new spot', 'new place', 'next adventure'],
translateTasks: ['translate', 'translation', 'language'],
creditScore: ['credit', 'score', 'credit report', 'credit score', 'raise your score'],
couponCodes: ['coupon', 'discount', 'save', 'coupon codes', 'discounts', 'save more'],
mattressDeals: ['sleep', 'mattress', 'mattresses', 'bed', 'bedding', 'sleep better'],
petSupplies: ['pet', 'furry', 'dog', 'cat', 'pet food', 'pet toys', 'furry friend'],
airportParking: ['park', 'parking', 'airport parking', 'airport', 'reserve parking'],
jewelrySearch: ['jewelry', 'jewellery', 'shine', 'necklace', 'ring', 'earrings', 'bracelet', 'stunning jewelry'],
};
static translateTasks() {
const words= ["hello", "world", "computer", "science", "artificial intelligence", "machine learning", "data science", "open source", "programming", "javascript", "python", "java", "ruby", "golang"];
const verbs = ["translate", "language translate", "translation of" ];
const languages = ["english", "hindi", "french", "spanish", "german", "japanese", "chinese", "arabic", "russian", "portuguese", "italian"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${words[Math.floor(Math.random() * words.length)]} in ${languages[Math.floor(Math.random() * languages.length)]}`;
}
static flightSearch() {
const words= ["flight", "flights"];
const verbs = ["search for", "find", "book", "cheap", "best", "towards", "near me", "to"];
const places = ["new york", "london", "paris", "tokyo", "sydney", "mumbai", "delhi", "beijing", "moscow", "cairo"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${words[Math.floor(Math.random() * words.length)]} ${places[Math.floor(Math.random() * places.length)]}`;
}
static weatherInCity() {
const cities = ["new york", "london", "paris", "tokyo", "sydney", "mumbai", "delhi", "beijing", "moscow", "cairo"];
const verbs = ["weather in", "temperature in", "climate in", "forecast for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${cities[Math.floor(Math.random() * cities.length)]}`;
}
static wordMeaning() {
const words = ["serendipity", "ephemeral", "eloquent", "resilient", "ubiquitous", "paradigm", "benevolent", "enigma", "nostalgia", "ambiguous"];
const verbs = ["meaning of", "definition of", "what is", "explain"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${words[Math.floor(Math.random() * words.length)]}`;
}
static localRestaurants() {
const cuisines = ["italian", "chinese", "mexican", "indian", "japanese", "thai", "french", "mediterranean", "korean", "vietnamese", "ethiopian", "greek", "brazilian", "turkish"];
const verbs = ["restaurants near me", "food near me", "places to eat near me", "dining near me", "eateries near me"];
const modifiers = ["best", "top rated", "authentic", "popular", "affordable", ""];
const modifier = modifiers[Math.floor(Math.random() * modifiers.length)];
return modifier ? `${modifier} ${cuisines[Math.floor(Math.random() * cuisines.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}` : `${cuisines[Math.floor(Math.random() * cuisines.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}`;
}
static loanDeals() {
const loanTypes = ["home", "car", "personal", "student", "business", "mortgage", "auto", "refinance", "payday", "consolidation"];
const verbs = ["loan rates", "loan deals", "loan offers", "loan comparison", "interest rates"];
const modifiers = ["best", "lowest", "affordable", "competitive", "cheap"];
return `${modifiers[Math.floor(Math.random() * modifiers.length)]} ${loanTypes[Math.floor(Math.random() * loanTypes.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}`;
}
static movie() {
const genres = ["action", "comedy", "thriller", "horror", "romance", "sci-fi", "drama", "fantasy", "mystery", "adventure"];
const years = ["2024", "2023", "2022", "classic", "new"];
return `best ${genres[Math.floor(Math.random() * genres.length)]} ${years[Math.floor(Math.random() * years.length)]} movies`;
}
static sportsScores() {
const sports = ["NBA", "NFL", "MLB", "NHL", "Premier League", "La Liga", "Champions League", "cricket", "tennis", "Formula 1", "NCAA", "MLS", "Serie A", "Bundesliga"];
const verbs = ["scores today", "results today", "live scores", "standings", "schedule today", "highlights"];
return `${sports[Math.floor(Math.random() * sports.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}`;
}
static lyricsSearch() {
const songs = ["Shape of You", "Blinding Lights", "Bohemian Rhapsody", "Imagine", "Hotel California", "Stairway to Heaven", "Smells Like Teen Spirit", "Billie Jean", "Sweet Child O Mine", "Hey Jude", "Wonderwall", "Yesterday", "Hallelujah", "Rolling in the Deep"];
const verbs = ["lyrics of", "song lyrics for", "words to", "lyrics to", "full lyrics of"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${songs[Math.floor(Math.random() * songs.length)]}`;
}
static famousPerson() {
const people = ["Albert Einstein", "Marie Curie", "Nelson Mandela", "Leonardo da Vinci", "Elon Musk", "Oprah Winfrey", "Steve Jobs", "Barack Obama", "Bill Gates", "Malala Yousafzai"];
const verbs = ["biography of", "facts about", "who is", "information about"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${people[Math.floor(Math.random() * people.length)]}`;
}
static famousQuote() {
const topics = ["success", "life", "love", "motivation", "wisdom", "happiness", "courage", "leadership", "perseverance", "friendship", "family", "faith", "strength", "change"];
const verbs = ["famous quotes about", "inspirational quotes about", "best quotes on", "quotes about", "sayings about"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${topics[Math.floor(Math.random() * topics.length)]}`;
}
static stockPrice() {
const stocks = ["AAPL", "GOOGL", "MSFT", "AMZN", "TSLA", "META", "NVDA", "NFLX", "AMD", "INTC"];
const verbs = ["current stock price of", "stock price of", "share price of", "stock quote for"];
return `${stocks[Math.floor(Math.random() * stocks.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}`;
}
static eventsNearMe() {
const events = ["concerts", "festivals", "theater shows", "comedy shows", "sports events", "art exhibitions", "food festivals", "conferences", "workshops", "meetups", "farmers markets", "live music", "plays"];
const verbs = ["near me", "this weekend", "tonight", "today", "coming up"];
const modifiers = ["upcoming", "best", "popular", "free", "local", ""];
const modifier = modifiers[Math.floor(Math.random() * modifiers.length)];
return modifier ? `${modifier} ${events[Math.floor(Math.random() * events.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}` : `${events[Math.floor(Math.random() * events.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]}`;
}
static hotelInCity() {
const cities = ["New York", "London", "Paris", "Tokyo", "Dubai", "Barcelona", "Singapore", "Rome", "Amsterdam", "Sydney", "Bangkok", "Istanbul", "Miami", "Las Vegas"];
const modifiers = ["best", "luxury", "cheap", "budget", "affordable", "top rated", "boutique"];
const verbs = ["hotels in", "places to stay in", "accommodations in", "resorts in"];
return `${modifiers[Math.floor(Math.random() * modifiers.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]} ${cities[Math.floor(Math.random() * cities.length)]}`;
}
static healthSymptom() {
const symptoms = ["headache", "fever", "cough", "fatigue", "sore throat", "back pain", "stomach ache", "dizziness", "insomnia", "allergies"];
const verbs = ["symptoms of", "treatment for", "causes of", "remedies for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${symptoms[Math.floor(Math.random() * symptoms.length)]}`;
}
static relalEstateNearMe() {
const types = ["houses", "apartments", "condos", "townhouses", "land", "commercial properties"];
const actions = ["for sale", "for rent", "listings"];
return `${types[Math.floor(Math.random() * types.length)]} ${actions[Math.floor(Math.random() * actions.length)]} near me`;
}
static currencyConversion() {
const currencies = [["USD", "EUR"], ["EUR", "GBP"], ["GBP", "JPY"], ["USD", "INR"], ["EUR", "JPY"], ["USD", "CAD"], ["AUD", "USD"], ["CHF", "EUR"], ["CNY", "USD"], ["USD", "MXN"]];
const pair = currencies[Math.floor(Math.random() * currencies.length)];
return `${Math.floor(Math.random() * 1000)} ${pair[0]} to ${pair[1]}`;
}
static recipeSearch() {
const dishes = ["chocolate cake", "pasta carbonara", "chicken curry", "pizza margherita", "sushi rolls", "beef stew", "apple pie", "lasagna", "pad thai", "tiramisu"];
const verbs = ["how to make", "recipe for", "easy", "best"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${dishes[Math.floor(Math.random() * dishes.length)]}`;
}
static latestNews() {
const topics = ["technology", "world", "politics", "business", "entertainment", "sports", "science", "health", "climate", "economy"];
const verbs = ["latest news on", "breaking news", "news about", "today's news on", "recent news about"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${topics[Math.floor(Math.random() * topics.length)]}`;
}
static jobSearch() {
const companies = ["Google", "Microsoft", "Apple", "Amazon", "Meta", "Tesla", "Netflix", "Adobe", "Intel", "IBM", "Oracle", "Salesforce"];
const verbs = ["open roles at", "jobs at", "careers at", "hiring at", "positions at"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${companies[Math.floor(Math.random() * companies.length)]}`;
}
static bookDeals() {
const genres = ["fiction", "mystery", "sci-fi", "romance", "thriller", "fantasy", "biography", "self-help", "history", "cookbook"];
const verbs = ["best deals on", "reviews for", "find", "buy", "top rated"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${genres[Math.floor(Math.random() * genres.length)]} books`;
}
static craftSupplies() {
const crafts = ["DIY kits", "knitting supplies", "painting supplies", "scrapbooking materials", "jewelry making kits", "woodworking tools", "sewing supplies", "paper crafts", "pottery kits", "embroidery supplies"];
const verbs = ["find", "shop for", "buy", "deals on", "best"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${crafts[Math.floor(Math.random() * crafts.length)]}`;
}
static shoppingDeals() {
const items = ["laptops", "headphones", "coffee maker", "running shoes", "smartwatch", "backpack", "desk chair", "blender", "vacuum cleaner", "gaming mouse"];
const verbs = ["deals on", "best price for", "shop for", "buy", "find"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${items[Math.floor(Math.random() * items.length)]}`;
}
static insurancePlans() {
const types = ["health", "life", "auto", "home", "dental", "vision", "pet", "travel", "disability", "renters"];
const verbs = ["best", "affordable", "compare", "top rated", "cheap"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${types[Math.floor(Math.random() * types.length)]} insurance plans`;
}
static carDeals() {
const brands = ["Toyota", "Honda", "Ford", "Tesla", "BMW", "Mercedes", "Hyundai", "Nissan", "Mazda", "Volkswagen"];
const verbs = ["deals on", "used", "new", "certified pre-owned", "best price for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${brands[Math.floor(Math.random() * brands.length)]} cars`;
}
static creditCards() {
const types = ["travel rewards", "cash back", "no annual fee", "balance transfer", "student", "business", "secured", "low interest"];
const verbs = ["best", "top rated", "compare", "find", "apply for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${types[Math.floor(Math.random() * types.length)]} credit cards`;
}
static internetPlans() {
const providers = ["Xfinity", "AT&T", "Verizon", "Spectrum", "Cox", "Optimum", "Frontier", "CenturyLink"];
const verbs = ["plans from", "deals from", "compare", "best", "affordable"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} internet ${providers[Math.floor(Math.random() * providers.length)]}`;
}
static homeUpgrades() {
const projects = ["kitchen remodel", "bathroom renovation", "flooring installation", "new roof", "solar panels", "new windows", "deck building", "basement finishing", "HVAC upgrade", "landscaping"];
const verbs = ["tools for", "tips for", "deals on", "cost of", "best contractors for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${projects[Math.floor(Math.random() * projects.length)]}`;
}
static flowerDelivery() {
const occasions = ["birthday", "anniversary", "sympathy", "get well", "congratulations", "thank you", "romantic", "just because"];
const types = ["roses", "lilies", "tulips", "orchids", "mixed bouquet", "flower arrangement"];
const verbs = ["send", "delivery", "order", "buy", "same day"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${types[Math.floor(Math.random() * types.length)]} for ${occasions[Math.floor(Math.random() * occasions.length)]}`;
}
static concertTickets() {
const artists = ["Taylor Swift", "Ed Sheeran", "Beyoncé", "Drake", "Coldplay", "The Weeknd", "Ariana Grande", "BTS", "Billie Eilish", "Harry Styles"];
const verbs = ["tickets for", "concert tickets", "tour dates", "find tickets for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${artists[Math.floor(Math.random() * artists.length)]}`;
}
static cruiseDeals() {
const destinations = ["Caribbean", "Alaska", "Mediterranean", "Mexico", "Hawaii", "Bahamas", "Europe", "Asia", "Australia", "South America"];
const lines = ["Royal Caribbean", "Carnival", "Norwegian", "Princess", "Disney", "MSC"];
const verbs = ["cruise to", "deals on cruise to", "book cruise to", "best cruise to"];
const includeLine = Math.random() > 0.5;
return includeLine
? `${lines[Math.floor(Math.random() * lines.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]} ${destinations[Math.floor(Math.random() * destinations.length)]}`
: `${verbs[Math.floor(Math.random() * verbs.length)]} ${destinations[Math.floor(Math.random() * destinations.length)]}`;
}
static gamingSearch() {
const games = ["Call of Duty", "Minecraft", "FIFA", "GTA 5", "Fortnite", "Elden Ring", "The Legend of Zelda", "Cyberpunk 2077", "God of War", "Spider-Man", "Hogwarts Legacy", "Baldur's Gate 3"];
const verbs = ["best", "reviews for", "how to play", "tips for", "latest news about", "gameplay guide for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${games[Math.floor(Math.random() * games.length)]}`;
}
static streamingSearch() {
const services = ["Netflix", "Disney Plus", "Hulu", "HBO Max", "Apple TV Plus", "Amazon Prime Video", "Peacock", "Paramount Plus"];
const verbs = ["best shows on", "new movies on", "new releases on", "what to watch on", "top series on"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${services[Math.floor(Math.random() * services.length)]}`;
}
static phonePlans() {
const carriers = ["Verizon", "AT&T", "T-Mobile", "Mint Mobile", "Cricket Wireless", "Boost Mobile", "Google Fi"];
const verbs = ["best", "affordable", "compare", "unlimited", "cheapest"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} cell phone plans ${carriers[Math.floor(Math.random() * carriers.length)]}`;
}
static bankingSearch() {
const accountTypes = ["checking account", "savings account", "high yield savings", "money market account", "CD rates", "online bank account"];
const verbs = ["best", "compare", "top rated", "no fee", "high interest"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${accountTypes[Math.floor(Math.random() * accountTypes.length)]}`;
}
static locationExplore() {
const activities = ["bing maps"]
const verbs = ["directions to", "map to"];
const locations = ["tokyo", "paris", "new york", "sydney", "london", "rome", "barcelona", "dubai", "singapore", "amsterdam"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${activities[Math.floor(Math.random() * activities.length)]} ${locations[Math.floor(Math.random() * locations.length)]}`;
}
static creditScore() {
const verbs = ["check", "view", "monitor", "improve", "raise"];
const terms = ["credit score", "credit report", "credit rating", "FICO score", "credit check"];
const modifiers = ["free", "online", "best ways to", "how to", "tips to"];
return `${modifiers[Math.floor(Math.random() * modifiers.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]} ${terms[Math.floor(Math.random() * terms.length)]}`;
}
static couponCodes() {
const stores = ["Amazon", "Target", "Walmart", "Best Buy", "Macy's", "Nike", "Kohl's", "Home Depot", "Costco", "Nordstrom"];
const verbs = ["coupon codes for", "discounts at", "promo codes for", "deals at", "coupons for"];
const modifiers = ["latest", "best", "working", "today's", "exclusive"];
return `${modifiers[Math.floor(Math.random() * modifiers.length)]} ${verbs[Math.floor(Math.random() * verbs.length)]} ${stores[Math.floor(Math.random() * stores.length)]}`;
}
static mattressDeals() {
const brands = ["Casper", "Purple", "Tempur-Pedic", "Nectar", "Saatva", "Sleep Number", "Beautyrest", "Serta", "Tuft & Needle", "Helix"];
const verbs = ["best", "top rated", "deals on", "reviews for", "compare"];
const types = ["mattresses", "memory foam mattress", "hybrid mattress", "king mattress", "queen mattress"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${brands[Math.floor(Math.random() * brands.length)]} ${types[Math.floor(Math.random() * types.length)]}`;
}
static petSupplies() {
const pets = ["dog", "cat", "puppy", "kitten", "pet"];
const items = ["toys", "food", "treats", "beds", "collars", "grooming supplies", "gear", "accessories", "leashes", "bowls"];
const verbs = ["best", "top rated", "buy", "find", "shop for"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${pets[Math.floor(Math.random() * pets.length)]} ${items[Math.floor(Math.random() * items.length)]}`;
}
static airportParking() {
const airports = ["LAX", "JFK", "ORD", "ATL", "DFW", "SFO", "MIA", "SEA", "DEN", "BOS"];
const verbs = ["reserve", "book", "find", "compare", "cheap"];
const types = ["airport parking", "long term parking", "parking rates", "parking deals", "parking near"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${types[Math.floor(Math.random() * types.length)]} ${airports[Math.floor(Math.random() * airports.length)]}`;
}
static jewelrySearch() {
const types = ["necklaces", "rings", "earrings", "bracelets", "pendants", "watches", "diamond jewelry", "gold jewelry", "silver jewelry", "pearl jewelry"];
const verbs = ["find", "shop for", "buy", "deals on", "best"];
const occasions = ["for anniversary", "for birthday", "for wedding", "for Valentine's Day", "for Mother's Day", "for any occasion"];
return `${verbs[Math.floor(Math.random() * verbs.length)]} ${types[Math.floor(Math.random() * types.length)]} ${occasions[Math.floor(Math.random() * occasions.length)]}`;
}
/**
* Find the best matching search method based on keywords in title and description
* @param {string} title - The task title
* @param {string} description - The task description
* @returns {string} - The name of the matching method
*/
static findBestMatch(title, description) {
const combinedText = (title + " " + description).toLowerCase();
let bestMatch = null;
let maxMatches = 0;
// Create instance to access keywordsMap
const instance = new ExploreOnBing();
for (const [methodName, keywords] of Object.entries(instance.keywordsMap)) {
let matches = 0;
for (const keyword of keywords) {
if (combinedText.includes(keyword.toLowerCase())) {
matches++;
}
}
if (matches > maxMatches) {
maxMatches = matches;
bestMatch = methodName;
}
}
console.log(`[ExploreOnBing] Best match for "${title}": ${bestMatch} (${maxMatches} keyword matches)`);
// If no good match found, use a generic search
if (maxMatches === 0) {
console.warn(`[ExploreOnBing] No keyword matches found for "${title}", using generic search`);
return null;
}
return bestMatch;
}
/**
* Get search string for a given task
* @param {string} title - The task title
* @param {string} description - The task description
* @returns {string} - The search string to use
*/
static getSearchString(title, description) {
const methodName = ExploreOnBing.findBestMatch(title, description);
if (methodName && typeof ExploreOnBing[methodName] === 'function') {
return { searchString: ExploreOnBing[methodName](), matchedFunction: methodName };
}
// Fallback: extract search intent from description
// Pattern: "Search on Bing for/to TASK" or "Search using Bing for/to TASK"
const match = description.match(/search\b.*?\b(?:for|to)\s+(.+?)(?:\.|$)/i);
if (match) {
const fallback = match[1].trim();
pushQueueDebugLog(`Unmatched: "${title}" — using: "${fallback}"`, "unmatched");
return { searchString: fallback, matchedFunction: "descriptionFallback" };
}
// Last resort: use title as search
pushQueueDebugLog(`Unmatched: "${title}" — no description pattern, using title`, "unmatched");
return { searchString: title, matchedFunction: "titleFallback" };
}
}
// Configuration options for the user script
const configurations = [
{
id: "timeout-range",
name: "Search Delay",
type: "range",
value: TIMEOUT_RANGE,
range: [1, 60],
description: "Random delay between searches (seconds). Keep above 5s to avoid rate limits.",
},
{
id: "under-cooldown",
name: "Cooldown Mode",
type: "checkbox",
value: UNDER_COOLDOWN,
description: "Enable if experiencing 15-min cooldown restrictions. Adds 15-min wait after every 4 searches.",
},
{
id: "cooldown-timeout",
name: "Cooldown Wait",
type: "slider",
value: COOLDOWN_TIMEOUT,
range: [3, 30],
disabled: !UNDER_COOLDOWN,
description: "Minutes to wait after every 4th search when Cooldown Mode is enabled.",
},
{
id: "open-random-links",
name: "Open Random Links",
type: "checkbox",
value: OPEN_RANDOM_LINKS,
description: "Opens random search results in iframes to simulate human behavior and reduce restrictions.",
},
{
id: "auto-close-tabs",
name: "Auto-Close Tabs",
type: "checkbox",
value: getAutoCloseTabs(),
description: "Automatically close all tabs/windows opened by the script after completion.",
},
];
// Load previous searches from local storage or initialize an empty array
var searches = GM_getValue("searches", []);
// store the list of window handles or urls to close them later
var tabsToClose = GM_getValue("tabsToClose", []);
// Adjust timeout if under cooldown and within search limit
if (UNDER_COOLDOWN && searches.length % 4 == 0 && searches.length > 0) {
TIMEOUT = COOLDOWN_TIMEOUT * 60000; // mins * 60 secs
}
// Check if the current page is Bing search page
const isSearchPage = window.location.href.startsWith("https://www.bing.com/search");
// Check if the current page is Bing rewards dashboard page
const isDashboardPage = window.location.href.startsWith("https://rewards.bing.com/dashboard");
// Check if the current page is Bing rewards earn page
const isEarnPage = window.location.href.startsWith("https://rewards.bing.com/earn");
// Check if the current page is any Bing rewards page
const isRewardPage = isDashboardPage || isEarnPage;
// Check whether current device is a mobile or not
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const pageUrlParams = new URLSearchParams(window.location.search);
// Automation markers live in the URL hash fragment so they're not sent to Bing's servers.
const pageHashParams = new URLSearchParams(window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash);
const IS_AUTOMATION_CHILD_TAB = pageHashParams.get("ag_child") === "1";
const AUTOMATION_TAB_KIND = pageHashParams.get("ag_kind") || "";
const ACTIVE_EXPLORE_TASK_KEY = "activeExploreTaskId";
/**
* Persistent reward task queue and runner
* Keeps tasks across page navigations and reloads
*/
const REWARD_TASK_QUEUE_KEY = "rewardTaskQueue";
const REWARD_RUNNER_KEY = "rewardTaskRunner";
const QUEUE_DEBUG_LOG_KEY = "queueDebugLog";
const RUNNER_LEASE_TTL_MS = 180000;
const runnerInstanceId = `runner_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
let rewardQueueRunnerStarted = false;
/*=============================================*\
|* AUTOMATION STATE MACHINE *|
\*=============================================*/
const AUTOMATION_STATE_KEY = "automationState";
const AUTOMATION_RUNNING_KEY = "automationRunning";
const PHASES = {
IDLE: "idle",
PLAN_DASHBOARD: "phase_plan_dashboard",
PLAN_EARN: "phase_plan_earn",
PLAN_EXPLORE: "phase_plan_explore",
PLAN_POINTS: "phase_plan_points",
PLAN_SEED_SEARCH: "phase_plan_seed_search",
EXECUTE: "phase_execute",
COMPLETE: "phase_complete"
};
function isAutomationRunning() {
return GM_getValue(AUTOMATION_RUNNING_KEY, false);
}
function setAutomationRunning(value) {
GM_setValue(AUTOMATION_RUNNING_KEY, !!value);
if (typeof renderStartStopButton === "function") renderStartStopButton();
}
const DEFAULT_AUTOMATION_STATE = {
phase: PHASES.IDLE,
startedAt: null,
searchesNeeded: null,
searchesCompleted: 0,
searchTerms: [],
error: null
};
function getAutomationState() {
const state = GM_getValue(AUTOMATION_STATE_KEY, DEFAULT_AUTOMATION_STATE);
// Auto-reset if stuck for more than 30 minutes
if (state.phase !== PHASES.IDLE && state.startedAt && (Date.now() - state.startedAt > 30 * 60 * 1000)) {
console.warn("[AutoGrind] Automation state is stale (>30min). Resetting to idle.");
resetAutomationState();
return { ...DEFAULT_AUTOMATION_STATE };
}
return state;
}
function setAutomationState(updates) {
const current = getAutomationState();
const next = { ...current, ...updates };
GM_setValue(AUTOMATION_STATE_KEY, next);
if (updates.phase && updates.phase !== current.phase) {
console.log(`[AutoGrind] Phase transition: ${current.phase} -> ${next.phase}`);
pushQueueDebugLog(`Phase: ${current.phase} → ${next.phase}`);
}
return next;
}
function resetAutomationState() {
GM_setValue(AUTOMATION_STATE_KEY, { ...DEFAULT_AUTOMATION_STATE });
}
/** Map each phase to the URL where it should execute */
function getPhaseUrl(phase) {
switch (phase) {
case PHASES.PLAN_DASHBOARD: return "https://rewards.bing.com/dashboard";
case PHASES.PLAN_EARN: return "https://rewards.bing.com/earn";
case PHASES.PLAN_EXPLORE: return "https://rewards.bing.com/earn";
case PHASES.PLAN_POINTS: return "https://rewards.bing.com/earn";
case PHASES.PLAN_SEED_SEARCH: return "https://rewards.bing.com/earn";
case PHASES.EXECUTE: return "https://rewards.bing.com/earn";
default: return null;
}
}
/** Navigate to the correct page for the current phase if not already there */
function navigateForPhase(phase) {
const targetUrl = getPhaseUrl(phase);
if (!targetUrl) return false;
if (!window.location.href.startsWith(targetUrl)) {
pushQueueDebugLog(`Navigating to ${targetUrl} for ${phase}`);
window.location.href = targetUrl;
return true; // navigated away
}
return false; // already on correct page
}
/** Wait for a DOM element to appear, returning a promise */
function waitForElementAsync(selector, timeoutMs = 10000) {
return new Promise((resolve) => {
const existing = document.querySelector(selector);
if (existing) { resolve(existing); return; }
let elapsed = 0;
const interval = setInterval(() => {
elapsed += 300;
const el = document.querySelector(selector);
if (el) {
clearInterval(interval);
resolve(el);
} else if (elapsed >= timeoutMs) {
clearInterval(interval);
resolve(null);
}
}, 300);
});
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Cancellable timer registry. Every setTimeout/setInterval used by the automation
// pipeline must register here so stopAutomation() can cancel pending work, and so
// callbacks short-circuit when automation has been stopped between schedule and fire.
const timerHandles = new Set();
function scheduleTimeout(fn, ms) {
const handle = setTimeout(() => {
timerHandles.delete(handle);
if (!isAutomationRunning()) return;
fn();
}, ms);
timerHandles.add(handle);
return handle;
}
function scheduleInterval(fn, ms) {
const handle = setInterval(() => {
if (!isAutomationRunning()) {
clearInterval(handle);
timerHandles.delete(handle);
return;
}
fn();
}, ms);
timerHandles.add(handle);
return handle;
}
function clearAllTimers() {
timerHandles.forEach(h => {
clearTimeout(h);
clearInterval(h);
});
timerHandles.clear();
}
// Append automation marker params to a URL hash fragment. The hash is local-only
// (browsers never send it to servers) so Bing's tracking sees the unmodified URL,
// but the userscript can still identify its own tabs via location.hash.
function withHashParams(url, params = {}) {
try {
const parsed = new URL(url, window.location.origin);
const hashParams = new URLSearchParams(parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash);
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== "") {
hashParams.set(key, String(value));
}
});
parsed.hash = hashParams.toString();
return parsed.toString();
} catch (error) {
console.warn(`[AutoGrind] Could not append hash params to URL: ${url}`, error);
return url;
}
}
// Fetch headlines from the BBC World RSS feed (host allowlisted via @connect).
// Returns an array of strings on success, [] on any failure — caller falls back
// to generators.
function fetchNewsHeadlines() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://feeds.bbci.co.uk/news/world/rss.xml",
timeout: 8000,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, "text/xml");
const headlines = Array.from(doc.querySelectorAll("item > title"))
.map(el => (el.textContent || "").trim())
.filter(s => s.length > 0);
resolve(headlines);
} catch (e) {
console.warn("[News] Parse failed:", e);
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([]),
});
});
}
// Slice each headline into a few random 3-6 word snippets, shuffle, dedupe,
// then take the first `count`. Snippets are cleaned of trailing punctuation
// so they look like queries someone would actually type.
function snippetsFromHeadlines(headlines, count) {
const snippets = new Set();
const cleanWord = w => w.replace(/^[^\w]+|[^\w]+$/g, "");
const shuffled = [...headlines].shuffle();
for (const headline of shuffled) {
if (snippets.size >= count) break;
const words = headline.split(/\s+/).map(cleanWord).filter(w => w.length > 1);
if (words.length < 3) continue;
// Yield a few (1-3) snippets per headline to maximize variety from a small set.
const yields = 1 + Math.floor(Math.random() * 3);
for (let i = 0; i < yields && snippets.size < count; i++) {
const len = 3 + Math.floor(Math.random() * 4); // 3..6
const maxStart = Math.max(0, words.length - len);
const start = Math.floor(Math.random() * (maxStart + 1));
const snippet = words.slice(start, start + len).join(" ").trim();
if (snippet && snippet.split(" ").length >= 3) snippets.add(snippet);
}
}
return Array.from(snippets).slice(0, count);
}
function getRewardTaskQueue() {
return GM_getValue(REWARD_TASK_QUEUE_KEY, []);
}
function saveRewardTaskQueue(queue) {
GM_setValue(REWARD_TASK_QUEUE_KEY, queue);
}
function generateTaskId(prefix = "task") {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}
function enqueueRewardTask(task) {
const queue = getRewardTaskQueue();
const duplicate = queue.find(existing =>
existing.status !== "complete" &&
existing.type === task.type &&
existing.url === task.url &&
existing.searchString === task.searchString
);
if (duplicate) {
return duplicate.id;
}
const normalizedTask = {
id: generateTaskId("reward"),
title: task.title || "Untitled task",
type: task.type,
url: task.url || null,
searchString: task.searchString || null,
source: task.source || "unknown",
status: "pending",
timestamp: Date.now(),
updatedAt: Date.now(),
error: null,
exploreTaskId: null
};
queue.push(normalizedTask);
saveRewardTaskQueue(queue);
console.log(`[RewardQueue] Added ${normalizedTask.type} task ${normalizedTask.id}: "${normalizedTask.title}"`);
pushQueueDebugLog(`Enqueued ${normalizedTask.type} task: ${normalizedTask.title}`);
return normalizedTask.id;
}
function getNextPendingRewardTask() {
const queue = getRewardTaskQueue();
return queue.find(task => task.status === "pending") || null;
}
function updateRewardTask(taskId, updater) {
const queue = getRewardTaskQueue();
const taskIndex = queue.findIndex(task => task.id === taskId);
if (taskIndex === -1) return null;
const currentTask = queue[taskIndex];
const updatedTask = updater(currentTask);
queue[taskIndex] = {
...currentTask,
...updatedTask,
updatedAt: Date.now()
};
saveRewardTaskQueue(queue);
return queue[taskIndex];
}
function markRewardTaskStatus(taskId, status, extra = {}) {
return updateRewardTask(taskId, task => ({ ...task, status, ...extra }));
}
function clearRewardTaskQueue() {
saveRewardTaskQueue([]);
GM_setValue("exploreTaskQueue", []);
GM_setValue(REWARD_RUNNER_KEY, null);
console.log("[RewardQueue] Cleared reward and explore queues");
pushQueueDebugLog("Cleared reward and explore queues", "warn");
}
function cleanupRewardTaskQueue() {
const queue = getRewardTaskQueue();
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
const cleanedQueue = queue.filter(task => !(task.status === "complete" && task.updatedAt < thirtyMinutesAgo));
if (cleanedQueue.length !== queue.length) {
saveRewardTaskQueue(cleanedQueue);
console.log(`[RewardQueue] Cleaned ${queue.length - cleanedQueue.length} completed tasks`);
}
}
function getQueueStats() {
const rewardQueue = getRewardTaskQueue();
const exploreQueue = getExploreTaskQueue();
const rewards = {
pending: rewardQueue.filter(task => task.status === "pending").length,
inProgress: rewardQueue.filter(task => task.status === "in_progress").length,
complete: rewardQueue.filter(task => task.status === "complete").length,
failed: rewardQueue.filter(task => task.status === "failed").length,
total: rewardQueue.length
};
const explore = {
pending: exploreQueue.filter(task => task.status === "pending").length,
inProgress: exploreQueue.filter(task => task.status === "in_progress").length,
complete: exploreQueue.filter(task => task.status === "complete").length,
total: exploreQueue.length
};
return { rewards, explore };
}
function getQueueDebugLogs() {
return GM_getValue(QUEUE_DEBUG_LOG_KEY, []);
}
function pushQueueDebugLog(message, level = "info") {
const logs = getQueueDebugLogs();
logs.push({ ts: Date.now(), level, message });
const maxLogs = 250;
const nextLogs = logs.slice(-maxLogs);
GM_setValue(QUEUE_DEBUG_LOG_KEY, nextLogs);
}
function clearQueueDebugLogs() {
GM_setValue(QUEUE_DEBUG_LOG_KEY, []);
}
function claimRunnerLease() {
const now = Date.now();
const lease = GM_getValue(REWARD_RUNNER_KEY, null);
const leaseExpired = !lease || (now - lease.ts > RUNNER_LEASE_TTL_MS);
if (leaseExpired || lease.id === runnerInstanceId) {
GM_setValue(REWARD_RUNNER_KEY, { id: runnerInstanceId, ts: now });
return true;
}
return false;
}
function refreshRunnerLease() {
const lease = GM_getValue(REWARD_RUNNER_KEY, null);
if (lease && lease.id === runnerInstanceId) {
GM_setValue(REWARD_RUNNER_KEY, { id: runnerInstanceId, ts: Date.now() });
}
}
function isRunnerOwner() {
const lease = GM_getValue(REWARD_RUNNER_KEY, null);
return !!lease && lease.id === runnerInstanceId;
}
async function runRewardTask(task) {
const closeTimeout = getRandomTimeout();
if (task.type === "direct") {
if (!task.url) {
pushQueueDebugLog(`Direct task missing URL: ${task.title}`, "error");
markRewardTaskStatus(task.id, "failed", { error: "Missing task URL" });
return;
}
const taskUrl = withHashParams(task.url, { ag_child: "1", ag_kind: "direct", ag_task_id: task.id });
console.log(`[RewardQueue] Running direct task ${task.id}: ${taskUrl}`);
pushQueueDebugLog(`Opening direct task: ${task.title}`);
GM_openInTab(taskUrl, { active: false });
if (getAutoCloseTabs()) addTabToClose(taskUrl, closeTimeout);
let directWaitElapsed = 0;
const directWaitTotal = closeTimeout + 1200;
while (directWaitElapsed < directWaitTotal && isRunnerOwner() && isAutomationRunning()) {
await sleep(1000);
directWaitElapsed += 1000;
refreshRunnerLease();
}
if (!isAutomationRunning()) return;
markRewardTaskStatus(task.id, "complete");
pushQueueDebugLog(`Completed direct task: ${task.title}`);
return;
}
if (task.type === "explore") {
console.log(`[RewardQueue] Running explore task ${task.id}: "${task.searchString}"`);
pushQueueDebugLog(`Running explore task: ${task.title}`);
const exploreTaskId = addExploreTask(task.searchString, task.title);
markRewardTaskStatus(task.id, "in_progress", { exploreTaskId });
const exploreUrl = withHashParams(task.url || "https://www.bing.com/", {
ag_child: "1",
ag_kind: "explore",
ag_explore_task_id: exploreTaskId,
ag_parent_task_id: task.id
});
GM_openInTab(exploreUrl, { active: false });
if (getAutoCloseTabs()) addTabToClose(exploreUrl, closeTimeout + 10000);
const maxWait = 90000;
const pollInterval = 500;
let elapsed = 0;
while (elapsed < maxWait && isAutomationRunning()) {
await sleep(pollInterval);
elapsed += pollInterval;
refreshRunnerLease();
if (isTaskComplete(exploreTaskId)) {
markRewardTaskStatus(task.id, "complete");
pushQueueDebugLog(`Completed explore task: ${task.title}`);
return;
}
}
if (!isAutomationRunning()) return;
markRewardTaskStatus(task.id, "failed", { error: "Explore task timed out" });
pushQueueDebugLog(`Explore task timed out: ${task.title}`, "warn");
return;
}
if (task.type === "search") {
if (!task.searchString) {
pushQueueDebugLog(`Search task missing query: ${task.title}`, "error");
markRewardTaskStatus(task.id, "failed", { error: "Missing search string" });
return;
}
console.log(`[RewardQueue] Running search task ${task.id}: "${task.searchString}"`);
pushQueueDebugLog(`Running search task: ${task.searchString}`);
const searchChildId = addExploreTask(task.searchString, task.title);
markRewardTaskStatus(task.id, "in_progress", { exploreTaskId: searchChildId });
const searchUrl = withHashParams(generateSearchUrl(task.searchString), {
ag_child: "1",
ag_kind: "search",
ag_explore_task_id: searchChildId
});
GM_openInTab(searchUrl, { active: false });
if (getAutoCloseTabs()) addTabToClose(searchUrl, closeTimeout + 10000);
const maxWait = 90000;
const pollInterval = 500;
let elapsed = 0;
while (elapsed < maxWait && isAutomationRunning()) {
await sleep(pollInterval);
elapsed += pollInterval;
refreshRunnerLease();
if (isTaskComplete(searchChildId)) {
markRewardTaskStatus(task.id, "complete");
const currentState = getAutomationState();
setAutomationState({
searchesCompleted: (currentState.searchesCompleted || 0) + 1
});
pushQueueDebugLog(`Completed search task: "${task.searchString}"`);
return;
}
}
if (!isAutomationRunning()) return;
markRewardTaskStatus(task.id, "failed", { error: "Search task timed out" });
pushQueueDebugLog(`Search task timed out: "${task.searchString}"`, "warn");
return;
}
markRewardTaskStatus(task.id, "failed", { error: `Unsupported task type: ${task.type}` });
pushQueueDebugLog(`Unsupported task type encountered: ${task.type}`, "error");
}
async function startRewardQueueRunner() {
if (rewardQueueRunnerStarted) return;
if (!isAutomationRunning()) {
console.log("[RewardQueue] Automation not running. Runner not started.");
return;
}
if (!claimRunnerLease()) {
console.log("[RewardQueue] Another tab is running the queue");
pushQueueDebugLog("Runner start skipped: another tab owns the lease", "warn");
return;
}
rewardQueueRunnerStarted = true;
console.log("[RewardQueue] Runner started");
pushQueueDebugLog(`Runner started in tab ${runnerInstanceId}`);
while (isRunnerOwner() && isAutomationRunning()) {
cleanupRewardTaskQueue();
cleanupCompletedTasks();
refreshRunnerLease();
const nextTask = getNextPendingRewardTask();
if (!nextTask) {
break;
}
markRewardTaskStatus(nextTask.id, "in_progress", { error: null });
try {
await runRewardTask(nextTask);
} catch (error) {
console.error("[RewardQueue] Task failed", error);
pushQueueDebugLog(`Task failed: ${nextTask.title} (${String(error?.message || error)})`, "error");
markRewardTaskStatus(nextTask.id, "failed", { error: String(error?.message || error) });
}
refreshRunnerLease();
await sleep(1000);
}
if (isRunnerOwner()) {
GM_setValue(REWARD_RUNNER_KEY, null);
}
rewardQueueRunnerStarted = false;
console.log("[RewardQueue] Runner stopped");
pushQueueDebugLog("Runner stopped");
// If the queue drained naturally while in EXECUTE phase, transition to COMPLETE.
if (isAutomationRunning()) {
const remaining = getRewardTaskQueue().filter(t => t.status === "pending" || t.status === "in_progress");
if (remaining.length === 0 && getAutomationState().phase === PHASES.EXECUTE) {
setAutomationState({ phase: PHASES.COMPLETE });
await executeCompletePhase();
}
}
}
// Add shuffle method to Array prototype
Array.prototype.shuffle = function() {
const array = [...this];
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
/*=============================================*\
|* MAIN UI *|
\*=============================================*/
/**
* Create a container for the stop/enable, start, and settings buttons.
* These buttons now appear on all pages and have improved styling.
*/
const autoSearchContainer = document.createElement("div");
autoSearchContainer.classList.add("auto-search-container");
const searchIcon = document.createElement("div");
searchIcon.classList.add("search-icon", "modern-button");
const PLAY_ICON_HTML = `
<svg class="button-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
<span>Start</span>
`;
const STOP_ICON_HTML = `
<svg class="button-icon" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="6" width="12" height="12" rx="2"/>
</svg>
<span>Stop</span>
`;
function renderStartStopButton() {
const running = isAutomationRunning();
searchIcon.innerHTML = running ? STOP_ICON_HTML : PLAY_ICON_HTML;
searchIcon.title = running ? "Stop automation" : "Start Auto-Search!!";
searchIcon.classList.toggle("running", running);
}
renderStartStopButton();
searchIcon.addEventListener("click", () => {
if (isAutomationRunning()) {
stopAutomation();
} else {
startSearch();
}
});
// Pick up cross-tab state changes (Stop clicked in another tab) without needing GM_addValueChangeListener.
setInterval(renderStartStopButton, 1000);
const settingsIcon = document.createElement("div");
settingsIcon.classList.add("settings-icon", "modern-button");
settingsIcon.innerHTML = `
<svg class="button-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
<span>Settings</span>
`;
settingsIcon.title = "Configure Auto-Search Settings";
const queueIcon = document.createElement("div");
queueIcon.classList.add("queue-icon", "modern-button");
queueIcon.innerHTML = `
<svg class="button-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
</svg>
<span>Queue</span>
`;
queueIcon.title = "Open Queue Monitor";
const rateIcon = document.createElement("div");
rateIcon.classList.add("rate-icon", "modern-button", "mini-button");
rateIcon.innerHTML = `
<svg class="button-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</svg>
`;
rateIcon.title = "Rate this script on OpenUserJS";
const reportIcon = document.createElement("div");
reportIcon.classList.add("report-icon", "modern-button", "mini-button");
reportIcon.innerHTML = `
<svg class="button-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/>
</svg>
`;
reportIcon.title = "Report a problem";
// UI buttons only appear on reward pages (dashboard and earn)
if (isRewardPage) {
autoSearchContainer.appendChild(searchIcon);
autoSearchContainer.appendChild(settingsIcon);
autoSearchContainer.appendChild(queueIcon);
// Group the smaller secondary buttons in a row, slightly inset from the main column.
const miniRow = document.createElement("div");
miniRow.classList.add("mini-button-row");
miniRow.appendChild(rateIcon);
miniRow.appendChild(reportIcon);
autoSearchContainer.appendChild(miniRow);
}
// Add menu entries for Search and Settings
GM_registerMenuCommand('Start / Stop Auto-Search', function() {
if (isAutomationRunning()) stopAutomation();
else startSearch();
}, 's');
GM_registerMenuCommand('Settings', function() {
settingsOverlay.style.display = 'flex';
}, 'c');
GM_registerMenuCommand('Queue Monitor', function() {
openQueueOverlay();
}, 'q');
/**
* Create a settings overlay to configure the user script.
* The settings overlay contains a list of configuration options that can be adjusted by the user.
* The settings are stored in the local storage and are used to update the script's behavior.
*/
const settingsOverlay = document.createElement("div");
settingsOverlay.classList.add("settings-overlay");
settingsOverlay.style = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at top, rgba(15, 23, 42, 0.55), rgba(0, 0, 0, 0.7)); backdrop-filter: blur(14px) saturate(140%); -webkit-backdrop-filter: blur(14px) saturate(140%); display: none; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s ease;`;
const settingsContent = document.createElement("div");
settingsContent.classList.add("settings-content");
settingsContent.style = `background: linear-gradient(180deg, #344361 0%, #2D3C59 50%, #1f2b42 100%); padding: 0; border-radius: 20px; display: flex; flex-direction: column; max-width: 680px; max-height: 80vh; box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.06); animation: slideUp 0.4s cubic-bezier(0.22, 1, 0.36, 1); overflow: hidden;`;
// Add header
const settingsHeader = document.createElement("div");
settingsHeader.classList.add("settings-header");
settingsHeader.innerHTML = `
<h2 style="margin: 0; color: white; font-size: 17px; font-weight: 700; letter-spacing: 0.2px; display: flex; align-items: center; gap: 10px;">
<svg style="width: 20px; height: 20px;" viewBox="0 0 24 24" fill="white">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
Settings
</h2>
<button class="settings-close-btn" title="Close Settings">
<svg viewBox="0 0 24 24" fill="white">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
`;
const settingsBody = document.createElement("div");
settingsBody.classList.add("settings-body");
settingsBody.style = `background-color: #f4f6fa; padding: 18px; overflow-y: auto; flex: 1;`;
const queueOverlay = document.createElement("div");
queueOverlay.classList.add("settings-overlay", "queue-overlay");
queueOverlay.style = `position: fixed; top: 90px; left: 190px; display: none; z-index: 10001; font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;`;
const queueOverlayContent = document.createElement("div");
queueOverlayContent.classList.add("settings-content", "queue-overlay-content");
queueOverlayContent.style = `background: linear-gradient(180deg, #344361 0%, #2D3C59 50%, #1f2b42 100%); padding: 0; border-radius: 16px; display: flex; flex-direction: column; width: 380px; max-height: 520px; box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.06); animation: slideUp 0.35s cubic-bezier(0.22, 1, 0.36, 1); overflow: hidden;`;
const queueOverlayHeader = document.createElement("div");
queueOverlayHeader.classList.add("settings-header");
queueOverlayHeader.innerHTML = `
<h2 style="margin: 0; color: white; font-size: 13.5px; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase; display: flex; align-items: center; gap: 8px;">
<svg style="width: 16px; height: 16px; opacity: 0.85;" viewBox="0 0 24 24" fill="white"><path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/></svg>
Queue Monitor
</h2>
<button class="settings-close-btn" title="Close Queue Monitor">
<svg viewBox="0 0 24 24" fill="white">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
`;
const queueOverlayTabs = document.createElement("div");
queueOverlayTabs.classList.add("settings-tabs");
queueOverlayTabs.innerHTML = `
<button class="settings-tab active" data-queue-tab="queue">Queue</button>
<button class="settings-tab" data-queue-tab="logs">Debug Log</button>
`;
const queuePanelBody = document.createElement("div");
queuePanelBody.classList.add("settings-body", "queue-debug-body");
queuePanelBody.style = `background-color: #f4f6fa; padding: 12px; overflow-y: auto; flex: 1;`;
const queueLogBody = document.createElement("div");
queueLogBody.classList.add("settings-body", "queue-debug-body");
queueLogBody.style = `background-color: #f4f6fa; padding: 12px; overflow-y: auto; flex: 1; display: none;`;
let queueDebugInterval = null;
let activeQueueOverlayTab = "queue";
function getStatusTagClass(status) {
if (status === "complete") return "status-complete";
if (status === "in_progress") return "status-progress";
if (status === "failed") return "status-failed";
return "status-pending";
}
// Map phase to a high-level "stage" label for the queue overlay header.
const PLAN_PHASES = new Set([
PHASES.PLAN_DASHBOARD, PHASES.PLAN_EARN, PHASES.PLAN_EXPLORE,
PHASES.PLAN_POINTS, PHASES.PLAN_SEED_SEARCH
]);
function getStageLabel(state) {
if (!isAutomationRunning()) return state.phase === PHASES.COMPLETE ? "Complete" : "Idle";
if (PLAN_PHASES.has(state.phase)) return "Planning";
if (state.phase === PHASES.EXECUTE) return "Executing";
if (state.phase === PHASES.COMPLETE) return "Complete";
return "Idle";
}
function renderQueueDebugPanel() {
const rewardQueue = getRewardTaskQueue();
const exploreQueue = getExploreTaskQueue();
const stats = getQueueStats();
const lease = GM_getValue(REWARD_RUNNER_KEY, null);
const autoState = getAutomationState();
const stageLabel = getStageLabel(autoState);
const phaseLabel = autoState.phase === PHASES.IDLE ? "Idle" : autoState.phase.replace("phase_", "").replace(/_/g, " ");
const inProgressTask = rewardQueue.find(t => t.status === "in_progress");
const finishedCount = stats.rewards.complete + stats.rewards.failed;
const totalCount = stats.rewards.total;
let stageDetail = stageLabel;
if (isAutomationRunning()) {
const progressFragment = totalCount > 0 ? ` ${finishedCount}/${totalCount}` : "";
const phaseFragment = stageLabel === "Planning" ? ` (${phaseLabel})` : "";
const taskFragment = inProgressTask ? ` — ${inProgressTask.title}` : "";
stageDetail = `${stageLabel}${progressFragment}${phaseFragment}${taskFragment}`;
}
const searchProgress = autoState.searchesNeeded != null
? `${autoState.searchesCompleted || 0}/${autoState.searchesNeeded}`
: "—";
const rewardRows = rewardQueue.length
? rewardQueue.map(task => `
<div class="queue-row">
<div class="queue-row-title">${task.title}</div>
<div class="queue-row-meta">
<span class="queue-type">${task.type}</span>
<span class="queue-status ${getStatusTagClass(task.status)}">${task.status}</span>
</div>
</div>
`).join("")
: `<div class="queue-empty">No reward tasks queued.</div>`;
const exploreRows = exploreQueue.length
? exploreQueue.map(task => `
<div class="queue-row small">
<div class="queue-row-title">${task.title}</div>
<div class="queue-row-meta">
<span class="queue-status ${getStatusTagClass(task.status)}">${task.status}</span>
</div>
</div>
`).join("")
: `<div class="queue-empty">No explore child tasks.</div>`;
queuePanelBody.innerHTML = `
<div class="queue-stage-banner">${stageDetail}</div>
<div class="queue-summary-grid">
<div class="queue-summary-card"><strong>Reward Total</strong><span>${stats.rewards.total}</span></div>
<div class="queue-summary-card"><strong>Pending</strong><span>${stats.rewards.pending}</span></div>
<div class="queue-summary-card"><strong>In Progress</strong><span>${stats.rewards.inProgress}</span></div>
<div class="queue-summary-card"><strong>Failed</strong><span>${stats.rewards.failed}</span></div>
</div>
<div class="queue-lease-row">Phase: <strong>${phaseLabel}</strong> | Searches: ${searchProgress}</div>
<div class="queue-lease-row">Orchestrator: ${lease ? lease.id : "none"}</div>
<div class="queue-section">
<div class="queue-section-header">
<h3>Reward Queue</h3>
<button class="queue-clear-btn" id="clear-queue-btn">Clear Queue</button>
</div>
<div class="queue-list">${rewardRows}</div>
</div>
<div class="queue-section">
<div class="queue-section-header">
<h3>Explore Subtasks</h3>
<span class="queue-count">${stats.explore.total} total</span>
</div>
<div class="queue-list">${exploreRows}</div>
</div>
`;
const clearButton = queuePanelBody.querySelector("#clear-queue-btn");
if (clearButton) {
clearButton.addEventListener("click", () => {
stopAutomation();
clearQueueDebugLogs();
renderQueueDebugPanel();
renderQueueDebugLogs();
});
}
}
function renderQueueDebugLogs() {
const logs = getQueueDebugLogs();
const rows = logs.length
? logs.slice().reverse().map(log => {
const time = new Date(log.ts).toLocaleTimeString();
return `<div class="queue-log-row ${log.level}"><span>[${time}]</span> ${log.message}</div>`;
}).join("")
: `<div class="queue-empty">No debug logs yet.</div>`;
queueLogBody.innerHTML = `
<div class="queue-section">
<div class="queue-section-header">
<h3>Debug Log</h3>
<button class="queue-clear-btn" id="clear-log-btn">Clear Log</button>
</div>
<div class="queue-log-list">${rows}</div>
</div>
`;
const clearLogButton = queueLogBody.querySelector("#clear-log-btn");
if (clearLogButton) {
clearLogButton.addEventListener("click", () => {
clearQueueDebugLogs();
renderQueueDebugLogs();
});
}
}
function setQueueOverlayTab(tabName) {
activeQueueOverlayTab = tabName;
queueOverlayTabs.querySelectorAll(".settings-tab").forEach(tab => {
tab.classList.toggle("active", tab.dataset.queueTab === tabName);
});
queuePanelBody.style.display = tabName === "queue" ? "block" : "none";
queueLogBody.style.display = tabName === "logs" ? "block" : "none";
if (tabName === "queue") renderQueueDebugPanel();
if (tabName === "logs") renderQueueDebugLogs();
}
function startQueueOverlayRefresh() {
if (queueDebugInterval) clearInterval(queueDebugInterval);
renderQueueDebugPanel();
renderQueueDebugLogs();
queueDebugInterval = setInterval(() => {
renderQueueDebugPanel();
renderQueueDebugLogs();
}, 2000);
}
function openQueueOverlay() {
QUEUE_OVERLAY_OPEN = true;
GM_setValue("queue-overlay-open", true);
setQueueOverlayTab(activeQueueOverlayTab || "queue");
queueOverlay.style.display = "block";
startQueueOverlayRefresh();
}
function closeQueueOverlay() {
QUEUE_OVERLAY_OPEN = false;
GM_setValue("queue-overlay-open", false);
if (queueDebugInterval) {
clearInterval(queueDebugInterval);
queueDebugInterval = null;
}
queueOverlay.style.animation = "fadeOut 0.3s ease";
queueOverlayContent.style.animation = "slideDown 0.3s ease";
setTimeout(() => {
queueOverlay.style.display = "none";
queueOverlay.style.animation = "";
queueOverlayContent.style.animation = "";
}, 300);
}
queueOverlayTabs.querySelectorAll(".settings-tab").forEach(tab => {
tab.addEventListener("click", () => setQueueOverlayTab(tab.dataset.queueTab));
});
configurations.forEach(config => {
const settingItem = document.createElement("div");
settingItem.classList.add("settings-item");
const settingCard = document.createElement("div");
settingCard.classList.add("setting-card");
const settingHeader = document.createElement("div");
settingHeader.classList.add("setting-card-header");
const name = document.createElement("div");
name.classList.add("settings-item-name");
name.textContent = config.name;
const currentValue = document.createElement("div");
currentValue.classList.add("settings-item-value");
settingHeader.appendChild(name);
settingHeader.appendChild(currentValue);
const inputContainer = document.createElement("div");
inputContainer.classList.add("settings-item-input");
let input;
if (config.type == "slider") {
input = document.createElement("input");
input.type = "range";
input.min = config.range[0];
input.max = config.range[1];
input.value = GM_getValue(config.id, config.value);
input.disabled = config.disabled;
input.classList.add("modern-slider");
} else if (config.type == "range") {
input = document.createElement("div"); input.classList.add("range-slider");
input.value = GM_getValue(config.id, config.value);
input.valueText = input.value.join("-");
for (let i = 0; i < 2; i++) {
let rangeInput = document.createElement("input");
Object.assign(rangeInput, { type: "range", value: config.value[i], min: config.range[0], max: config.range[1], style: `height: 1px;` });
rangeInput.classList.add("modern-slider");
input.appendChild(rangeInput);
rangeInput.addEventListener("input", () => {
if (parseInt(input.children[0].value) > parseInt(input.children[1].value)) input.children[i].value = input.children[1 - i].value; // Ensure min <= max
input.value = [input.children[0].value, input.children[1].value];
input.valueText = input.value.join("-");
});
}
} else if (config.type == "checkbox") {
input = document.createElement("input");
input.type = "checkbox";
input.checked = GM_getValue(config.id, config.value);
input.oninput = () => input.valueText = input.checked ? "Enabled" : "Disabled";
input.classList.add("modern-checkbox");
}
input.id = config.id;
input.dispatchEvent(new Event("input")); // Trigger input event to initialize `input.valueText`
currentValue.textContent = input.valueText??input.value;
input.addEventListener("input", () => {
GM_setValue(config.id, input.type == "checkbox" ? input.checked : input.value);
currentValue.textContent = input.valueText??input.value;
updateConfigVariable(config.id, input.type == "checkbox" ? input.checked : input.value);
});
inputContainer.appendChild(input);
const description = document.createElement("div");
description.classList.add("settings-item-description");
description.innerHTML = config.description;
settingCard.appendChild(settingHeader);
settingCard.appendChild(inputContainer);
settingCard.appendChild(description);
settingItem.appendChild(settingCard);
settingsBody.appendChild(settingItem);
});
settingsContent.appendChild(settingsHeader);
settingsContent.appendChild(settingsBody);
settingsOverlay.appendChild(settingsContent);
queueOverlayContent.appendChild(queueOverlayHeader);
queueOverlayContent.appendChild(queueOverlayTabs);
queueOverlayContent.appendChild(queuePanelBody);
queueOverlayContent.appendChild(queueLogBody);
queueOverlay.appendChild(queueOverlayContent);
// Function to close settings with animation
function closeSettings() {
settingsOverlay.style.animation = "fadeOut 0.3s ease";
settingsContent.style.animation = "slideDown 0.3s ease";
setTimeout(() => {
settingsOverlay.style.display = "none";
settingsOverlay.style.animation = "";
settingsContent.style.animation = "";
}, 300);
}
// Close button event listener
settingsHeader.querySelector(".settings-close-btn").addEventListener("click", closeSettings);
queueOverlayHeader.querySelector(".settings-close-btn").addEventListener("click", closeQueueOverlay);
if (isRewardPage) {
document.body.appendChild(settingsOverlay);
document.body.appendChild(queueOverlay);
// Restore queue overlay if it was previously open
if (QUEUE_OVERLAY_OPEN) {
openQueueOverlay();
}
}
settingsIcon.addEventListener("click", () => {
settingsOverlay.style.display = "flex";
});
queueIcon.addEventListener("click", () => {
openQueueOverlay();
});
// Close settings dialog when clicking outside the popup
settingsOverlay.addEventListener("mousedown", function (event) {
// Only close if clicking outside the settingsContent
if (event.target === settingsOverlay) {
closeSettings();
}
});
// Feedback dialog (Rate / Report). A single overlay, repopulated per call.
const feedbackOverlay = document.createElement("div");
feedbackOverlay.classList.add("feedback-dialog-overlay");
const feedbackDialog = document.createElement("div");
feedbackDialog.classList.add("feedback-dialog");
feedbackOverlay.appendChild(feedbackDialog);
if (isRewardPage) document.body.appendChild(feedbackOverlay);
function closeFeedbackDialog() {
feedbackOverlay.style.animation = "fadeOut 0.3s ease";
feedbackDialog.style.animation = "slideDown 0.3s var(--ag-ease)";
setTimeout(() => {
feedbackOverlay.style.display = "none";
feedbackOverlay.style.animation = "";
feedbackDialog.style.animation = "";
}, 300);
}
function showFeedbackDialog({ variant, iconSvg, title, body, primaryLabel, url }) {
feedbackDialog.innerHTML = `
<div class="feedback-dialog-icon ${variant}">${iconSvg}</div>
<h3>${title}</h3>
<p>${body}</p>
<div class="feedback-dialog-actions">
<button class="feedback-btn secondary" data-action="close">Maybe later</button>
<button class="feedback-btn primary ${variant}" data-action="open">${primaryLabel}</button>
</div>
`;
feedbackOverlay.style.display = "flex";
feedbackDialog.querySelector('[data-action="close"]').addEventListener("click", closeFeedbackDialog);
feedbackDialog.querySelector('[data-action="open"]').addEventListener("click", () => {
try { GM_openInTab(url, { active: true, insert: true }); }
catch { window.open(url, "_blank", "noopener,noreferrer"); }
closeFeedbackDialog();
});
}
feedbackOverlay.addEventListener("mousedown", (event) => {
if (event.target === feedbackOverlay) closeFeedbackDialog();
});
const STAR_SVG = `<svg viewBox="0 0 24 24" fill="white"><path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>`;
const REPORT_SVG = `<svg viewBox="0 0 24 24" fill="white"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/></svg>`;
rateIcon.addEventListener("click", () => {
showFeedbackDialog({
variant: "rate",
iconSvg: STAR_SVG,
title: "Enjoying the script?",
body: "A quick vote on OpenUserJS keeps the project alive and helps me find time to keep building. It only takes a moment — thank you so much for the support!",
primaryLabel: "Vote on OpenUserJS",
url: "https://openuserjs.org/scripts/amit/Bing_Rewards"
});
});
reportIcon.addEventListener("click", () => {
showFeedbackDialog({
variant: "report",
iconSvg: REPORT_SVG,
title: "Something not working?",
body: "Bug reports help me find and fix issues much faster than I ever could on my own. Even a short note about what went wrong is hugely valuable — thank you for taking the time.",
primaryLabel: "Open an issue",
url: "https://openuserjs.org/scripts/amit/Bing_Rewards/issues"
});
});
// Close settings with Escape key
document.addEventListener("keydown", function(event) {
if (event.key === "Escape" && settingsOverlay.style.display === "flex") {
closeSettings();
}
if (event.key === "Escape" && queueOverlay.style.display !== "none") {
closeQueueOverlay();
}
if (event.key === "Escape" && feedbackOverlay.style.display === "flex") {
closeFeedbackDialog();
}
});
// Add logic to enable/disable the cooldown-timeout input
const underCooldownEl = settingsOverlay.querySelector("#under-cooldown");
if (underCooldownEl) {
underCooldownEl.addEventListener("change", (event) => {
const cooldownInput = settingsOverlay.querySelector("#cooldown-timeout");
if (cooldownInput) cooldownInput.disabled = !event.target.checked;
});
}
/**
* This function updates the icon's appearance with the specified content and classlist.
* @param {string} content - The content to display in the icon.
* @param {string} classlist - The classlist to apply to the icon.
*/
function updateIcon(content, classlist="searching") {
searchIcon.classList.add(classlist);
settingsIcon.classList.add(classlist);
searchIcon.querySelector("span").textContent = content;
}
/**
* This function updates the configuration variables based on the user's input in the settings overlay.
* This is required only for configurations that require immediate changes before reloading the tab like the `Max Searches` option.
* @param {string} id - The id of the configuration variable to update.
* @param {string} value - The new value of the configuration variable.
*/
function updateConfigVariable(id, value) {
if (id === "cooldown-timeout") COOLDOWN_TIMEOUT = parseInt(value);
else if (id === "under-cooldown") UNDER_COOLDOWN = !!value;
}
/*=============================================*\
|* HELPER FUNCTIONS *|
\*=============================================*/
/**
* Initiate the automation flow.
* Resets state and begins the planning pipeline (no execution yet):
* Plan Dashboard -> Plan Earn -> Plan Explore -> Plan Points -> Plan Seed Search
* After planning completes, the queue runner drains all queued tasks one at a time.
*/
function startSearch() {
// Reset all state
resetAutomationState();
clearRewardTaskQueue();
searches = [];
GM_setValue("searches", searches);
tabsToClose = [];
GM_setValue("tabsToClose", tabsToClose);
setAutomationRunning(true);
// Initialize state machine - start planning from the dashboard
setAutomationState({
phase: PHASES.PLAN_DASHBOARD,
startedAt: Date.now()
});
pushQueueDebugLog("Start clicked. Beginning planning pipeline.");
// Navigate to dashboard to begin planning
if (isDashboardPage) {
waitForRewardPageReady(() => dispatchPhase(getAutomationState()));
} else {
window.location.href = "https://rewards.bing.com/dashboard";
}
}
/**
* Halt all automation work. Flips the running flag (so any in-flight callbacks
* short-circuit), cancels every pending timer, clears queues so future tab loads
* don't try to auto-close, and releases the runner lease.
*/
function stopAutomation() {
setAutomationRunning(false);
clearAllTimers();
resetAutomationState();
GM_setValue("tabsToClose", []);
tabsToClose = [];
GM_setValue("searches", []);
searches = [];
clearRewardTaskQueue();
GM_setValue(REWARD_RUNNER_KEY, null);
rewardQueueRunnerStarted = false;
pushQueueDebugLog("Automation stopped by user", "warn");
}
/*=============================================*\
|* COLLECTION FUNCTIONS *|
\*=============================================*/
// Daily-set tasks live on /dashboard. We enqueue them so the queue UI shows the plan,
// then click each <a> in place — clicking the real DOM element preserves the user-gesture
// + referer + cookie context Bing's tracking relies on. GM_openInTab loses that.
async function collectDashboardActivities() {
console.log("[Dashboard] Planning daily set activity cards...");
const root = document.querySelector("section#dailyset")
|| findSectionByHeading(/^(daily set|today'?s set)$/i);
if (!root) {
pushQueueDebugLog("[Dashboard] Daily set section NOT FOUND in DOM", "error");
return 0;
}
const allCards = root.querySelectorAll('a[data-rac][href], a[href*="bing.com/search"]');
if (allCards.length === 0) {
pushQueueDebugLog("[Dashboard] No anchor cards inside section", "warn");
return 0;
}
const taskEntries = [];
let completed = 0, noChip = 0, locked = 0, enqueued = 0, skippedTitleless = 0, blacklisted = 0;
for (const card of allCards) {
if (isCardCompleted(card)) { completed++; continue; }
if (isCardLocked(card)) { locked++; continue; }
const pts = getCardPoints(card);
if (!pts) { noChip++; continue; }
const title = extractCardTitle(card);
if (!title) { logTitlelessCard("[Dashboard]", card); skippedTitleless++; continue; }
if (isTaskBlacklisted(title)) {
pushQueueDebugLog(`[Dashboard] Blacklisted card skipped: "${title}"`);
blacklisted++;
continue;
}
const taskId = enqueueRewardTask({
type: "direct",
title: `${title} (+${pts})`,
url: card.href,
source: "dashboard"
});
taskEntries.push({ card, title, taskId });
enqueued++;
}
pushQueueDebugLog(`[Dashboard] cards=${allCards.length} completed=${completed} locked=${locked} noChip=${noChip} blacklisted=${blacklisted} enqueued=${enqueued} skippedTitleless=${skippedTitleless}`);
for (const { card, title, taskId } of taskEntries) {
if (!isAutomationRunning()) break;
try {
markRewardTaskStatus(taskId, "in_progress");
// Marker goes in the hash so Bing's server-side tracking sees the unmodified URL.
card.href = withHashParams(card.href, { ag_child: "1", ag_kind: "direct", ag_task_id: taskId });
card.target = "_blank";
card.click();
pushQueueDebugLog(`Clicked dashboard card: ${title}`);
const closeTimeout = getRandomTimeout();
if (getAutoCloseTabs()) addTabToClose(card.href, closeTimeout);
await sleep(closeTimeout + 1200);
if (!isAutomationRunning()) break;
markRewardTaskStatus(taskId, "complete");
} catch (error) {
console.error(`[Dashboard] Click failed for "${title}":`, error);
pushQueueDebugLog(`Click failed for "${title}": ${error.message}`, "error");
markRewardTaskStatus(taskId, "pending", { error: `Click failed: ${error.message}` });
}
}
return enqueued;
}
// Card heuristics. Microsoft rotates React class hashes; these helpers center the logic
// in one place so a future schema change is a single-file edit.
// "+N" chip identifies cards with claimable points. The chip element lives in a div
// with bg-statusInformativeTintBg; the number is the inner <p>'s text. Fallback:
// any "+\d+" text inside the card.
function getCardPoints(card) {
const chipEl = card.querySelector('.bg-statusInformativeTintBg');
if (chipEl) {
const m = (chipEl.textContent || '').match(/(\d+)/);
return m ? parseInt(m[1], 10) : 1;
}
const m2 = (card.textContent || '').match(/\+\s*(\d{1,4})(?!\d)/);
if (m2) return parseInt(m2[1], 10);
// Older schema: explicit "X pts" badge
if (/\d+\s*pts?\b/i.test(card.textContent || '')) return 1;
return 0;
}
function isCardCompleted(card) {
if (card.querySelector('.bg-statusSuccessRewardsBg, .bg-statusSuccessBg3')) return true;
return /\bCompleted\b/.test(card.textContent || '');
}
function isCardLocked(card) {
if (card.hasAttribute('data-disabled') || card.getAttribute('aria-disabled') === 'true') return true;
return /Available on \w+|Unlocks (?:tomorrow|on \w+)/i.test(card.textContent || '');
}
// Microsoft's Explore-on-Bing prompts follow "Search on Bing for/to X" — the X is the
// exact query they want. Falls back to null if the pattern doesn't match, in which
// case the caller can derive a query some other way.
function deriveSearchFromPrompt(description) {
if (!description) return null;
const cleaned = description.replace(//g, '').trim();
const m = cleaned.match(/^search on bing (?:for|to)\s+(.+?)[.\s]*$/i);
return m ? m[1].trim() : null;
}
function extractCardTitle(card) {
return card.querySelector('p.text-globalBody2Strong')?.textContent?.trim()
|| card.querySelector('p[class*="globalBody2Strong"]')?.textContent?.trim()
|| card.querySelector('p.text-body1Strong')?.textContent?.trim()
|| card.querySelector('p[class*="body1Strong"]')?.textContent?.trim()
|| card.getAttribute('aria-label')?.trim()
|| null;
}
function extractCardDescription(card) {
// Description is the second line-clamp paragraph (not the bold title).
const ps = card.querySelectorAll('p.line-clamp-3, p[class*="line-clamp"]');
for (const p of ps) {
if (/globalBody2Strong|body1Strong/.test(p.className)) continue;
const txt = p.textContent?.trim();
if (txt) return txt;
}
return '';
}
// If the card's href is a Bing search URL, return the query — that's the exact
// search Microsoft expects, no need to derive one from title/description.
function extractBingSearchQuery(url) {
try {
const u = new URL(url);
if (/(^|\.)bing\.com$/.test(u.host) && u.pathname === '/search' && u.searchParams.has('q')) {
return u.searchParams.get('q');
}
} catch {}
return null;
}
// Find a section by heading text, working with both old and new disclosure wrappers.
function findSectionByHeading(pattern) {
const heading = Array.from(document.querySelectorAll('h2,h3'))
.find(h => pattern.test((h.textContent || '').trim()));
if (!heading) return null;
return heading.closest('.react-aria-Disclosure')
|| heading.closest('section, [role="region"], div');
}
// Diagnostic dump for cards our selector matches but that have no visible title.
// Logs href + classes + a snippet of text so the offending card (e.g. an
// IAP_INACTIVE_TEMP_NOTIFY placeholder) can be identified without enqueueing it.
function logTitlelessCard(prefix, card) {
const href = card.getAttribute('href') || card.href || '';
const ariaLabel = card.getAttribute('aria-label') || '';
const className = card.getAttribute('class') || '';
const text = (card.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
const dataAttrs = Array.from(card.attributes)
.filter(a => a.name.startsWith('data-'))
.map(a => `${a.name}="${a.value}"`)
.join(' ');
console.warn(`${prefix} Skipping titleless card`, { href, ariaLabel, className, dataAttrs, text, card });
pushQueueDebugLog(`Skipped titleless card: ${text || ariaLabel || href || '(empty)'}`, "warn");
}
// Click any "Show more" toggle inside a section until every card is rendered.
// Bing collapses the Keep earning list past a fixed count; without this we'd miss
// the hidden cards entirely.
async function expandShowMoreToggles(root, prefix) {
let clicks = 0;
const maxClicks = 8;
while (clicks < maxClicks) {
const btn = Array.from(root.querySelectorAll('button')).find(b => {
if (b.disabled || b.getAttribute('aria-disabled') === 'true') return false;
return /^show more\b/i.test((b.textContent || '').trim());
});
if (!btn) break;
const beforeCount = root.querySelectorAll('a[data-rac][href]').length;
btn.click();
clicks++;
await sleep(800);
const afterCount = root.querySelectorAll('a[data-rac][href]').length;
pushQueueDebugLog(`${prefix} Clicked "Show more" (${clicks}): ${beforeCount} → ${afterCount} cards`);
if (afterCount === beforeCount) break;
}
}
// Earn-page Keep-earning cards. Same pattern as dashboard: enqueue all, then click
// each <a> in place. Microsoft mixes search-prompt cards in here too — those get an
// exploreTaskId so the child tab can match itself to the parent task.
async function collectEarnActivities() {
console.log("[Earn] Planning Keep earning cards...");
const root = findSectionByHeading(/^keep earning$/i)
|| document.querySelector("section#moreactivities");
if (!root) {
pushQueueDebugLog("[Earn] Keep earning section NOT FOUND in DOM", "error");
return 0;
}
await expandShowMoreToggles(root, "[Earn]");
const allCards = root.querySelectorAll('a[data-rac][href]');
if (allCards.length === 0) {
pushQueueDebugLog("[Earn] No anchor cards inside section", "warn");
return 0;
}
const taskEntries = [];
let completed = 0, noChip = 0, locked = 0, enqueued = 0, skippedTitleless = 0, blacklisted = 0;
for (const card of allCards) {
if (isCardCompleted(card)) { completed++; continue; }
if (isCardLocked(card)) { locked++; continue; }
const pts = getCardPoints(card);
if (!pts) { noChip++; continue; }
const title = extractCardTitle(card);
if (!title) { logTitlelessCard("[Earn]", card); skippedTitleless++; continue; }
if (isTaskBlacklisted(title)) {
pushQueueDebugLog(`[Earn] Blacklisted card skipped: "${title}"`);
blacklisted++;
continue;
}
const description = extractCardDescription(card);
const urlQuery = extractBingSearchQuery(card.href);
const promptQuery = deriveSearchFromPrompt(description);
// Only treat as a search card when there's a real signal (URL ?q= or prompt). The
// keyword generators below give varied queries, but they'd happily match any card
// containing a keyword — we don't want to "search" for a non-search activity card.
let searchQuery = null;
if (urlQuery || promptQuery) {
// Bing's new dashboard puts the card title into ?q=, so URL extraction yields the
// literal title. Prefer a keyword-generated query for variety; fall back to the
// literal URL/prompt text if no generator matches.
const generated = ExploreOnBing.getSearchString(title, description);
const isGenerated = generated.matchedFunction
&& generated.matchedFunction !== "titleFallback"
&& generated.matchedFunction !== "descriptionFallback";
searchQuery = isGenerated ? generated.searchString : (urlQuery || promptQuery);
}
if (searchQuery) {
const exploreTaskId = addExploreTask(searchQuery, title);
const taskId = enqueueRewardTask({
type: "explore",
title: `${title} (+${pts})`,
url: card.href,
searchString: searchQuery,
source: "earn"
});
taskEntries.push({ card, title, taskId, exploreTaskId, kind: "explore" });
} else {
const taskId = enqueueRewardTask({
type: "direct",
title: `${title} (+${pts})`,
url: card.href,
source: "earn"
});
taskEntries.push({ card, title, taskId, kind: "direct" });
}
enqueued++;
}
pushQueueDebugLog(`[Earn] cards=${allCards.length} completed=${completed} locked=${locked} noChip=${noChip} blacklisted=${blacklisted} enqueued=${enqueued} skippedTitleless=${skippedTitleless}`);
for (const entry of taskEntries) {
if (!isAutomationRunning()) break;
await clickAndWaitCard(entry, "[Earn]");
}
return enqueued;
}
// Shared click-and-wait used by earn + explore collectors. Performs the user-gesture
// click on the actual <a> element so Bing's tracking sees a real navigation; falls
// back to a fixed wait for direct cards or polls the explore-task queue for search prompts.
async function clickAndWaitCard(entry, prefix) {
const { card, title, taskId, exploreTaskId, kind } = entry;
try {
const hashParams = kind === "explore"
? { ag_child: "1", ag_kind: "explore", ag_explore_task_id: exploreTaskId, ag_parent_task_id: taskId }
: { ag_child: "1", ag_kind: "direct", ag_task_id: taskId };
markRewardTaskStatus(taskId, "in_progress", kind === "explore" ? { exploreTaskId } : {});
card.href = withHashParams(card.href, hashParams);
card.target = "_blank";
card.click();
pushQueueDebugLog(`Clicked ${kind} card: ${title}`);
const closeTimeout = getRandomTimeout();
if (getAutoCloseTabs()) addTabToClose(card.href, closeTimeout + (kind === "explore" ? 10000 : 0));
if (kind === "explore") {
const maxWait = 90000, pollInterval = 500;
let elapsed = 0, completed = false;
while (elapsed < maxWait && isAutomationRunning()) {
await sleep(pollInterval);
elapsed += pollInterval;
if (isTaskComplete(exploreTaskId)) { completed = true; break; }
}
if (!isAutomationRunning()) return;
if (completed) {
markRewardTaskStatus(taskId, "complete");
pushQueueDebugLog(`Completed ${kind} card: ${title}`);
} else {
markRewardTaskStatus(taskId, "failed", { error: "Explore task timed out" });
pushQueueDebugLog(`${kind} card timed out: ${title}`, "warn");
}
} else {
await sleep(closeTimeout + 1200);
if (!isAutomationRunning()) return;
markRewardTaskStatus(taskId, "complete");
pushQueueDebugLog(`Completed ${kind} card: ${title}`);
}
} catch (error) {
console.error(`${prefix} Click failed for "${title}":`, error);
pushQueueDebugLog(`Click failed for "${title}": ${error.message}`, "error");
markRewardTaskStatus(taskId, "pending", { error: `Click failed: ${error.message}` });
}
}
// Explore on Bing cards. Locked cards render as <span data-disabled="true">,
// so the a[data-rac][href] selector naturally excludes them; isCardLocked() also
// catches "Unlocks tomorrow"/aria-disabled in case Microsoft changes the markup.
async function collectExploreOnBingActivities() {
const root = document.querySelector('section#exploreonbing')
|| findSectionByHeading(/^explore on bing$/i);
if (!root) {
pushQueueDebugLog("[ExploreOnBing] No standalone section (now part of Keep earning)");
return 0;
}
const allCards = root.querySelectorAll('a[data-rac][href]:not([aria-disabled="true"])');
if (allCards.length === 0) {
pushQueueDebugLog("[ExploreOnBing] No unlocked anchor cards in section");
return 0;
}
cleanupCompletedTasks();
const taskEntries = [];
let completed = 0, noChip = 0, locked = 0, enqueued = 0, skippedTitleless = 0, blacklisted = 0;
for (const card of allCards) {
if (isCardCompleted(card)) { completed++; continue; }
if (isCardLocked(card)) { locked++; continue; }
const pts = getCardPoints(card);
if (!pts) { noChip++; continue; }
const title = extractCardTitle(card);
if (!title) { logTitlelessCard("[ExploreOnBing]", card); skippedTitleless++; continue; }
if (isTaskBlacklisted(title)) {
pushQueueDebugLog(`[ExploreOnBing] Blacklisted card skipped: "${title}"`);
blacklisted++;
continue;
}
const description = extractCardDescription(card);
// Bing's new dashboard puts the card title into ?q=, so URL extraction yields the
// literal title (looks like a useless "title search"). Prefer a keyword-generated
// query for variety; fall back to URL ?q=, then "Search on Bing for X" in the
// description, then getSearchString's own title/description fallback.
const urlQuery = extractBingSearchQuery(card.href);
const promptQuery = deriveSearchFromPrompt(description);
const generated = ExploreOnBing.getSearchString(title, description);
const isGenerated = generated.matchedFunction
&& generated.matchedFunction !== "titleFallback"
&& generated.matchedFunction !== "descriptionFallback";
const searchString = isGenerated
? generated.searchString
: (urlQuery || promptQuery || generated.searchString);
const exploreTaskId = addExploreTask(searchString, title);
const taskId = enqueueRewardTask({
type: "explore",
title: `${title} (+${pts})`,
url: card.href,
searchString,
source: "explore"
});
taskEntries.push({ card, title, taskId, exploreTaskId, kind: "explore" });
enqueued++;
}
pushQueueDebugLog(`[ExploreOnBing] cards=${allCards.length} completed=${completed} locked=${locked} noChip=${noChip} blacklisted=${blacklisted} enqueued=${enqueued} skippedTitleless=${skippedTitleless}`);
for (const entry of taskEntries) {
if (!isAutomationRunning()) break;
await clickAndWaitCard(entry, "[ExploreOnBing]");
}
return enqueued;
}
/*=============================================*\
|* PHASE EXECUTORS *|
\*=============================================*/
async function executePlanDashboardPhase() {
pushQueueDebugLog("Plan 1/5: Dashboard daily set");
await collectDashboardActivities();
setAutomationState({ phase: PHASES.PLAN_EARN });
scheduleTimeout(() => {
window.location.href = "https://rewards.bing.com/earn";
}, 1500);
}
async function executePlanEarnPhase() {
pushQueueDebugLog("Plan 2/5: Earn activities");
await collectEarnActivities();
setAutomationState({ phase: PHASES.PLAN_EXPLORE });
scheduleTimeout(() => dispatchPhase(getAutomationState()), 1500);
}
async function executePlanExplorePhase() {
pushQueueDebugLog("Plan 3/5: Explore on Bing");
await collectExploreOnBingActivities();
setAutomationState({ phase: PHASES.PLAN_POINTS });
scheduleTimeout(() => dispatchPhase(getAutomationState()), 1500);
}
async function executePlanPointsPhase() {
pushQueueDebugLog("Plan 4/5: Points breakdown");
const breakdownButton = findPointsBreakdownButton();
if (!breakdownButton) {
console.warn("[Plan-Points] Points breakdown button not found. Skipping searches.");
setAutomationState({ searchesNeeded: 0, phase: PHASES.EXECUTE });
await dispatchPhase(getAutomationState());
return;
}
breakdownButton.click();
pushQueueDebugLog("Clicked Points breakdown button");
// Wait for a dialog that actually contains breakdown content, not just any dialog
const dialog = await new Promise((resolve) => {
const searchLabels = ['bing search', 'pc search', 'mobile search'];
let elapsed = 0;
const interval = setInterval(() => {
elapsed += 300;
const candidates = document.querySelectorAll('[role="dialog"], section[role="dialog"]');
for (const el of candidates) {
const text = el.textContent.toLowerCase();
if (searchLabels.some(label => text.includes(label))) {
clearInterval(interval);
resolve(el);
return;
}
}
if (elapsed >= 10000) { clearInterval(interval); resolve(null); }
}, 300);
});
if (!dialog) {
console.warn("[Plan-Points] Points breakdown dialog did not appear. Skipping searches.");
setAutomationState({ searchesNeeded: 0, phase: PHASES.EXECUTE });
await dispatchPhase(getAutomationState());
return;
}
// Parse the dialog for the Bing-search row's X/Y. Fall back to 10 if the parser
// throws, returns 0, or returns null — Microsoft's dialog markup churns and a
// silently-zero parse would skip searches entirely, which we definitely don't want.
const FALLBACK_SEARCHES = 10;
let searchesNeeded;
try {
const parsed = parseBingSearchPoints(dialog);
if (parsed && parsed > 0) {
searchesNeeded = parsed;
pushQueueDebugLog(`Parsed ${searchesNeeded} searches needed from points breakdown`);
} else {
searchesNeeded = FALLBACK_SEARCHES;
pushQueueDebugLog(`Parser returned ${parsed} — falling back to ${FALLBACK_SEARCHES} searches`, "warn");
}
} catch (e) {
console.warn("[Plan-Points] parseBingSearchPoints threw:", e);
searchesNeeded = FALLBACK_SEARCHES;
pushQueueDebugLog(`Points parser failed (${e.message}) — falling back to ${FALLBACK_SEARCHES} searches`, "warn");
}
const closeBtn = dialog.querySelector('button[aria-label="Close"]') ||
dialog.querySelector('button[slot="close"]');
if (closeBtn) closeBtn.click();
pushQueueDebugLog(`Points breakdown: ${searchesNeeded} searches needed`);
setAutomationState({ phase: PHASES.PLAN_SEED_SEARCH, searchesNeeded });
scheduleTimeout(() => dispatchPhase(getAutomationState()), 1000);
}
function findPointsBreakdownButton() {
const allButtons = document.querySelectorAll('button[data-rac]');
for (const btn of allButtons) {
if (btn.textContent.toLowerCase().includes('points breakdown') && btn.hasAttribute('aria-expanded')) {
return btn;
}
}
const allPs = document.querySelectorAll('p');
for (const p of allPs) {
if (p.textContent.trim().toLowerCase() === 'points breakdown') {
const btn = p.closest('button');
if (btn) return btn;
}
}
return null;
}
function parseBingSearchPoints(dialog) {
const searchLabels = ['bing search', 'pc search', 'mobile search'];
const pointsPerSearch = 3;
// Find all leaf-level elements whose text exactly matches a search label,
// then walk up the DOM to find the label container and scan its next siblings
// for an "X/Y" points pattern (e.g. "45/60").
const allElements = dialog.querySelectorAll('*');
for (const el of allElements) {
if (el.children.length > 0) continue;
const text = el.textContent.trim().toLowerCase();
if (!searchLabels.includes(text)) continue;
// Try the element's parent, grandparent, etc. as a potential label cell
let container = el.parentElement;
while (container && container !== dialog) {
// Scan up to 4 next siblings (grid may have empty <span> cells between columns)
let sibling = container.nextElementSibling;
for (let i = 0; i < 4 && sibling; i++, sibling = sibling.nextElementSibling) {
const sibText = sibling.textContent.trim();
const match = sibText.match(/(\d+)\s*\/\s*(\d+)/);
if (match && parseInt(match[2], 10) > 0) {
const current = parseInt(match[1], 10);
const max = parseInt(match[2], 10);
const remaining = max - current;
const searchesNeeded = Math.ceil(remaining / pointsPerSearch);
console.log(`[PointsBreakdown] "${text}" ${current}/${max} → ${searchesNeeded} searches needed`);
return Math.max(0, searchesNeeded);
}
}
container = container.parentElement;
}
}
// Fallback: text scan — find X/Y within 150 chars of any search label
const dialogText = dialog.textContent;
for (const label of searchLabels) {
const idx = dialogText.toLowerCase().indexOf(label);
if (idx === -1) continue;
const nearby = dialogText.substring(idx, idx + 150);
const match = nearby.match(/(\d+)\s*\/\s*(\d+)/);
if (match) {
const current = parseInt(match[1], 10);
const max = parseInt(match[2], 10);
const remaining = max - current;
const searchesNeeded = Math.ceil(remaining / pointsPerSearch);
console.log(`[PointsBreakdown] (text fallback) "${label}" ${current}/${max} → ${searchesNeeded} searches needed`);
return Math.max(0, searchesNeeded);
}
}
console.warn("[PointsBreakdown] Could not parse Bing search row. Returning 0.");
return 0;
}
async function executePlanSeedSearchPhase() {
pushQueueDebugLog("Plan 5/5: Fetching news headlines for search terms");
const state = getAutomationState();
const searchesNeeded = state.searchesNeeded ?? 0;
if (searchesNeeded <= 0) {
setAutomationState({ phase: PHASES.EXECUTE, searchTerms: [], searchesCompleted: 0 });
await dispatchPhase(getAutomationState());
return;
}
let terms = [];
try {
const headlines = await fetchNewsHeadlines();
pushQueueDebugLog(`Fetched ${headlines.length} headlines from news API`);
terms = snippetsFromHeadlines(headlines, searchesNeeded);
} catch (e) {
console.warn("[SeedSearch] News fetch failed:", e);
}
// Top up with generators if the API was thin or failed.
if (terms.length < searchesNeeded) {
pushQueueDebugLog(`News yielded ${terms.length}/${searchesNeeded} terms — topping up with generators`, "warn");
const generators = [
ExploreOnBing.latestNews, ExploreOnBing.weatherInCity,
ExploreOnBing.sportsScores, ExploreOnBing.famousPerson,
ExploreOnBing.stockPrice, ExploreOnBing.recipeSearch,
ExploreOnBing.movie, ExploreOnBing.lyricsSearch
];
const seen = new Set(terms);
let i = 0;
while (terms.length < searchesNeeded && i < searchesNeeded * 3) {
const candidate = generators[i % generators.length]();
if (!seen.has(candidate)) { terms.push(candidate); seen.add(candidate); }
i++;
}
}
pushQueueDebugLog(`Prepared ${terms.length} search terms — enqueueing`);
for (const term of terms) {
enqueueRewardTask({
type: "search",
title: `Search: ${term}`,
searchString: term,
source: "seed_search"
});
}
setAutomationState({
phase: PHASES.EXECUTE,
searchTerms: terms,
searchesCompleted: 0
});
// No navigation needed — we never left the earn page. Just dispatch the next phase.
await dispatchPhase(getAutomationState());
}
// EXECUTE phase: kick off the queue runner and idle. The runner drains all queued
// tasks (direct, explore, search) one at a time. When it's done, the runner exits
// and the next time we land on a reward page with the queue empty, we transition to COMPLETE.
async function executeExecutePhase() {
const queue = getRewardTaskQueue();
const remaining = queue.filter(task => task.status !== "complete" && task.status !== "failed");
if (remaining.length === 0) {
pushQueueDebugLog("Queue drained. Moving to complete.");
setAutomationState({ phase: PHASES.COMPLETE });
await executeCompletePhase();
return;
}
// DEBUG: confirm the planned queue before executing. Re-enable when debugging.
// if (!confirmPlannedQueueOnce(remaining)) {
// pushQueueDebugLog("User declined planned queue — halting before execute. Queue preserved for review.", "warn");
// // Halt without wiping the queue so the user can inspect what was planned.
// setAutomationRunning(false);
// clearAllTimers();
// GM_setValue(REWARD_RUNNER_KEY, null);
// rewardQueueRunnerStarted = false;
// return;
// }
pushQueueDebugLog(`Execute: ${remaining.length} task(s) queued`);
startRewardQueueRunner();
}
// DEBUG-ONLY: blocks before execution starts so you can review the planned queue.
// Uses a session flag so it only fires once per planning cycle (not on every tab reload).
function confirmPlannedQueueOnce(remaining) {
const RUN_KEY = "executeConfirmedAt";
const state = getAutomationState();
const startedAt = state.startedAt || 0;
const lastConfirmed = GM_getValue(RUN_KEY, 0);
if (lastConfirmed && lastConfirmed >= startedAt) return true;
const summary = remaining.map((t, i) => ` ${i + 1}. [${t.type}] ${t.title}`).join("\n");
const ok = confirm(
`Planning complete. About to execute ${remaining.length} task(s):\n\n${summary}\n\nProceed?`
);
if (ok) GM_setValue(RUN_KEY, Date.now());
return ok;
}
async function executeCompletePhase() {
const state = getAutomationState();
const searchCount = state.searchesCompleted || 0;
pushQueueDebugLog(`All phases complete! ${searchCount} searches performed.`);
console.log("[AutoGrind] All phases complete!");
resetAutomationState();
setAutomationRunning(false);
GM_setValue("searches", []);
if (isRewardPage) {
const notification = document.createElement("div");
notification.classList.add("script-notification");
notification.textContent = `All done! ${searchCount} searches completed.`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 5000);
}
}
/*=============================================*\
|* PHASE DISPATCHER *|
\*=============================================*/
function waitForRewardPageReady(callback) {
let attempts = 0;
const maxAttempts = 15;
const checkInterval = setInterval(() => {
attempts++;
const hasContent = document.querySelector("section#dailyset") ||
document.querySelector("section#moreactivities") ||
document.querySelector("h2");
if (hasContent || attempts >= maxAttempts) {
clearInterval(checkInterval);
callback();
}
}, 1000);
}
async function dispatchPhase(state) {
if (!isAutomationRunning()) {
console.log("[Dispatcher] Automation not running. Skipping dispatch.");
return;
}
console.log(`[Dispatcher] Phase: ${state.phase}, Page: ${isDashboardPage ? 'dashboard' : isEarnPage ? 'earn' : 'other'}`);
switch (state.phase) {
case PHASES.PLAN_DASHBOARD:
if (navigateForPhase(PHASES.PLAN_DASHBOARD)) return;
await executePlanDashboardPhase();
break;
case PHASES.PLAN_EARN:
if (navigateForPhase(PHASES.PLAN_EARN)) return;
await executePlanEarnPhase();
break;
case PHASES.PLAN_EXPLORE:
if (navigateForPhase(PHASES.PLAN_EXPLORE)) return;
await executePlanExplorePhase();
break;
case PHASES.PLAN_POINTS:
if (navigateForPhase(PHASES.PLAN_POINTS)) return;
await executePlanPointsPhase();
break;
case PHASES.PLAN_SEED_SEARCH:
// Seed terms come from a news API (no navigation) — execute in place.
if (navigateForPhase(PHASES.PLAN_SEED_SEARCH)) return;
await executePlanSeedSearchPhase();
break;
case PHASES.EXECUTE:
if (navigateForPhase(PHASES.EXECUTE)) return;
await executeExecutePhase();
break;
case PHASES.COMPLETE:
await executeCompletePhase();
break;
default:
console.log(`[Dispatcher] Nothing to do for phase: ${state.phase}`);
}
}
/**
* Wait for elements to appear on the page and execute a callback function when they are found.
* This function repeatedly checks for the presence of the specified selectors on the page.
* Once any of the selectors are found, the callback function is called with the selector as a parameter.
* @param {Array} selectors - The selectors to wait for.
* @param {Function} callback - The callback function to execute when the selectors are found.
*/
function waitForElements(selectors, callback) {
if (selectors == null) {
callback(null);
return;
}
for (let selector of selectors) {
if (document.querySelector(selector)) {
callback(selector);
return;
}
}
setTimeout(() => waitForElements(selectors, callback), 500);
}
/**
* Add a tab to the list of tabs to close after a specified timeout.
* This function adds the tab to the [tabsToClose] array and sets a timeout to close the tab.
* The tabs to close are stored in the local storage and are closed after the specified timeout.
* @param {Window} tab - The tab to close.
* @param {number} timeout - The timeout in milliseconds to close the tab.
* @example addTabToClose(window.open("https://rewards.bing.com/dashboard", "_blank"), 5000);
*/
function addTabToClose(tab, timeout=5000) {
tabsToClose.push(
{"url": tab, "timeout": timeout}
);
GM_setValue("tabsToClose", tabsToClose);
}
/**
* Get the Bing search URL for a given search term.
* This function constructs the Bing search URL with the specified search term and returns it.
* @param {string} searchTerm - The search term to include in the URL.
* @returns {string} - The Bing search URL for the given search term.
*/
function generateSearchUrl(searchTerm) {
return `https://www.bing.com/search?FORM=U523DF&PC=U523&q=${encodeURI(searchTerm)}&FORM=ANNTA1&qs=ds`;
}
/*=============================================*\
|* EXPLORE TASK QUEUE HELPERS *|
\*=============================================*/
/**
* Get the explore task queue from storage
* @returns {Array} - Array of task objects
*/
function getExploreTaskQueue() {
return GM_getValue('exploreTaskQueue', []);
}
/**
* Save the explore task queue to storage
* @param {Array} queue - Array of task objects
*/
function saveExploreTaskQueue(queue) {
GM_setValue('exploreTaskQueue', queue);
}
/**
* Add a new task to the explore task queue
* @param {string} searchString - The search string to execute
* @param {string} title - The task title for logging
* @returns {string} - The unique task ID
*/
function addExploreTask(searchString, title) {
const queue = getExploreTaskQueue();
const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
const task = {
id: taskId,
searchString: searchString,
title: title,
status: 'pending',
timestamp: Date.now()
};
queue.push(task);
saveExploreTaskQueue(queue);
console.log(`[ExploreTaskQueue] Added task ${taskId}: "${title}"`);
return taskId;
}
/**
* Get the next pending task from the queue
* @returns {Object|null} - The next pending task or null
*/
function getNextPendingTask() {
const queue = getExploreTaskQueue();
return queue.find(task => task.status === 'pending') || null;
}
function getExploreTaskById(taskId) {
if (!taskId) return null;
const queue = getExploreTaskQueue();
return queue.find(task => task.id === taskId) || null;
}
/**
* Mark a task as in progress
* @param {string} taskId - The task ID
*/
function markTaskInProgress(taskId) {
const queue = getExploreTaskQueue();
const task = queue.find(t => t.id === taskId);
if (task) {
task.status = 'in_progress';
saveExploreTaskQueue(queue);
console.log(`[ExploreTaskQueue] Task ${taskId} marked as in_progress`);
}
}
/**
* Mark a task as complete
* @param {string} taskId - The task ID
*/
function markTaskComplete(taskId) {
const queue = getExploreTaskQueue();
const task = queue.find(t => t.id === taskId);
if (task) {
task.status = 'complete';
saveExploreTaskQueue(queue);
console.log(`[ExploreTaskQueue] Task ${taskId} marked as complete`);
}
}
/**
* Find a task by search string
* @param {string} searchString - The search string to match
* @returns {Object|null} - The matching task or null
*/
function findTaskBySearchString(searchString) {
const queue = getExploreTaskQueue();
return queue.find(task =>
task.searchString === searchString &&
task.status === 'in_progress'
) || null;
}
/**
* Check if a specific task is complete
* @param {string} taskId - The task ID
* @returns {boolean} - True if task is complete
*/
function isTaskComplete(taskId) {
const queue = getExploreTaskQueue();
const task = queue.find(t => t.id === taskId);
return task ? task.status === 'complete' : false;
}
/**
* Clean up old completed tasks (older than 5 minutes)
*/
function cleanupCompletedTasks() {
const queue = getExploreTaskQueue();
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
const cleanedQueue = queue.filter(task =>
!(task.status === 'complete' && task.timestamp < fiveMinutesAgo)
);
if (cleanedQueue.length !== queue.length) {
saveExploreTaskQueue(cleanedQueue);
console.log(`[ExploreTaskQueue] Cleaned up ${queue.length - cleanedQueue.length} old tasks`);
}
}
/**
* Debug helper to show current queue state
*/
function debugQueue() {
const queue = getExploreTaskQueue();
console.log(`[ExploreTaskQueue] Current queue state (${queue.length} tasks):`);
queue.forEach((task, index) => {
console.log(` [${index + 1}] ${task.id} - "${task.title}" - ${task.status} - "${task.searchString}"`);
});
return queue;
}
// Make debug function globally available (call window.debugExploreQueue() from console)
window.debugExploreQueue = debugQueue;
/*=============================================*\
|* MAIN SCRIPT *|
\*=============================================*/
/**
* Handle "Explore on Bing" task search execution
* When a new tab opens from an "Explore on Bing" task, fill the search form and submit
*/
if ((window.location.pathname === '/' && window.location.host === 'www.bing.com') && IS_AUTOMATION_CHILD_TAB && AUTOMATION_TAB_KIND === "explore") {
const exploreTaskId = pageHashParams.get("ag_explore_task_id");
const nextTask = getExploreTaskById(exploreTaskId);
if (nextTask && (nextTask.status === "pending" || nextTask.status === "in_progress")) {
console.log(`[ExploreOnBing] Detected explore task ${nextTask.id}: "${nextTask.title}"`);
console.log(`[ExploreOnBing] Search string: "${nextTask.searchString}"`);
console.log(`[ExploreOnBing] Current URL: ${window.location.href}`);
// Mark task as in progress
markTaskInProgress(nextTask.id);
GM_setValue(ACTIVE_EXPLORE_TASK_KEY, nextTask.id);
// Wait for the search form to load
let formCheckAttempts = 0;
const maxFormCheckAttempts = 20;
const checkForForm = () => {
if (!isAutomationRunning()) return;
formCheckAttempts++;
console.log(`[ExploreOnBing] Attempt ${formCheckAttempts}/${maxFormCheckAttempts} to find search form`);
// Try multiple selectors for the search box
const searchBox = document.querySelector('textarea#sb_form_q') ||
document.querySelector('textarea.sb_form_q') ||
document.querySelector('input.b_searchbox') ||
document.querySelector('textarea.b_searchbox');
if (searchBox) {
console.log("[ExploreOnBing] Search box found, filling and submitting");
searchBox.value = nextTask.searchString;
const exploreSearchUrl = withHashParams(generateSearchUrl(nextTask.searchString), {
ag_child: "1",
ag_kind: "explore",
ag_explore_task_id: nextTask.id
});
setTimeout(() => {
if (isAutomationRunning()) window.location.href = exploreSearchUrl;
}, 300);
} else if (formCheckAttempts < maxFormCheckAttempts) {
setTimeout(checkForForm, 500);
} else {
console.error(`[ExploreOnBing] ERROR: Search form not found after ${maxFormCheckAttempts} attempts!`);
if (isAutomationRunning()) {
window.location.href = withHashParams(generateSearchUrl(nextTask.searchString), {
ag_child: "1",
ag_kind: "explore",
ag_explore_task_id: nextTask.id
});
}
}
};
checkForForm();
} else if (exploreTaskId) {
console.warn(`[ExploreOnBing] Explore task ${exploreTaskId} not available. Closing tab.`);
if (getAutoCloseTabs() && isAutomationRunning()) window.close();
}
}
/**
* Wait for the page to load the points element first.
* For android, this step is skipped.
* Bing seems to have 2 different selectors for the points element
* based on which browser is being used, so the possible selectors are
* put inside [pointsElem]
* In case of mobile, the script skips searching for the element.
*/
try {
// Add the button container only on reward pages
if (isRewardPage) {
document.body.appendChild(autoSearchContainer);
}
if (isSearchPage) {
const urlParams = new URLSearchParams(window.location.search);
const currentSearchQuery = urlParams.get('q');
// Automation markers live in the hash fragment, not the query string.
const requestedExploreTaskId = pageHashParams.get('ag_explore_task_id') || GM_getValue(ACTIVE_EXPLORE_TASK_KEY, null);
console.log(`[ExploreOnBing] Search page loaded, query: "${currentSearchQuery}"`);
// Handle explore child tabs
if (IS_AUTOMATION_CHILD_TAB && AUTOMATION_TAB_KIND === "explore" && currentSearchQuery) {
const task = getExploreTaskById(requestedExploreTaskId) || findTaskBySearchString(currentSearchQuery);
console.log(`[ExploreOnBing] Task lookup result:`, task ? `Found ${task.id}` : 'Not found');
if (task) {
console.log(`[ExploreOnBing] This is explore task ${task.id}: "${task.title}"`);
console.log(`[ExploreOnBing] Search query: "${currentSearchQuery}"`);
if (OPEN_RANDOM_LINKS) {
try {
let searchLinks = isMobile
? document.querySelectorAll(".b_algoheader > a")
: document.querySelectorAll("li.b_algo h2 a");
if (searchLinks.length > 0) {
const excludeDomains = ["britannica.com", "sunshineseeker.com"];
searchLinks = Array.from(searchLinks).filter(link => {
const metaElement = link.closest(".b_algo")?.querySelector(".b_tpcn div.tpmeta");
return !metaElement || !excludeDomains.some(domain => metaElement.innerText.includes(domain));
});
if (searchLinks.length > 0) {
let randLink = searchLinks[Math.floor(Math.random() * searchLinks.length)];
console.log(`[ExploreOnBing] Opening random link in iframe: ${randLink.href}`);
let iframe = document.createElement("iframe");
iframe.name = "randLinkFrame";
iframe.style.width = "100%";
iframe.style.height = "600px";
randLink.parentElement.appendChild(iframe);
randLink.target = "randLinkFrame";
randLink.click();
}
}
} catch (e) {
console.error("[ExploreOnBing] Error opening random link:", e);
}
}
const waitTime = getRandomTimeout();
console.log(`[ExploreOnBing] Waiting ${waitTime}ms before closing`);
if (getAutoCloseTabs() && isAutomationRunning()) showAutoCloseRing(waitTime);
setTimeout(() => {
if (!isAutomationRunning()) {
console.log("[ExploreOnBing] Automation stopped, skipping completion + close");
return;
}
console.log(`[ExploreOnBing] Marking task ${task.id} as complete`);
markTaskComplete(task.id);
if (GM_getValue(ACTIVE_EXPLORE_TASK_KEY, null) === task.id) {
GM_setValue(ACTIVE_EXPLORE_TASK_KEY, null);
}
if (getAutoCloseTabs()) {
console.log("[ExploreOnBing] Closing tab");
window.close();
}
}, waitTime);
throw new Error("ExploreTaskHandled");
} else {
console.log("[ExploreOnBing] Child explore tab query did not match a task");
}
}
// Handle search child tabs (opened by the queue runner during EXECUTE phase)
if (IS_AUTOMATION_CHILD_TAB && AUTOMATION_TAB_KIND === "search" && currentSearchQuery) {
const searchTaskId = pageHashParams.get('ag_explore_task_id');
const task = getExploreTaskById(searchTaskId);
console.log(`[SearchChild] Task lookup:`, task ? `Found ${task.id}` : 'Not found');
if (task) {
console.log(`[SearchChild] Processing search task ${task.id}: "${currentSearchQuery}"`);
markTaskInProgress(task.id);
if (OPEN_RANDOM_LINKS) {
try {
let searchLinks = isMobile
? document.querySelectorAll(".b_algoheader > a")
: document.querySelectorAll("li.b_algo h2 a");
if (searchLinks.length > 0) {
const excludeDomains = ["britannica.com", "sunshineseeker.com"];
searchLinks = Array.from(searchLinks).filter(link => {
const metaElement = link.closest(".b_algo")?.querySelector(".b_tpcn div.tpmeta");
return !metaElement || !excludeDomains.some(domain => metaElement.innerText.includes(domain));
});
if (searchLinks.length > 0) {
let randLink = searchLinks[Math.floor(Math.random() * searchLinks.length)];
console.log(`[SearchChild] Opening random link in iframe: ${randLink.href}`);
let iframe = document.createElement("iframe");
iframe.name = "randLinkFrame";
iframe.style.width = "100%";
iframe.style.height = "600px";
randLink.parentElement.appendChild(iframe);
randLink.target = "randLinkFrame";
randLink.click();
}
}
} catch (e) {
console.error("[SearchChild] Error opening random link:", e);
}
}
const waitTime = getRandomTimeout();
console.log(`[SearchChild] Waiting ${waitTime}ms before marking complete`);
if (getAutoCloseTabs() && isAutomationRunning()) showAutoCloseRing(waitTime);
setTimeout(() => {
if (!isAutomationRunning()) {
console.log("[SearchChild] Automation stopped, skipping completion + close");
return;
}
console.log(`[SearchChild] Marking task ${task.id} as complete`);
markTaskComplete(task.id);
if (getAutoCloseTabs()) {
console.log("[SearchChild] Closing tab");
window.close();
}
}, waitTime);
throw new Error("SearchTaskHandled");
} else {
console.log("[SearchChild] Search task not found, closing");
if (getAutoCloseTabs() && isAutomationRunning()) window.close();
}
}
// PLAN_SEED_SEARCH no longer runs on search pages — it now fetches news via API
// from the earn page directly. Nothing to do here.
}
if (isRewardPage) {
if (IS_AUTOMATION_CHILD_TAB) {
console.log(`[AutoGrind] Child tab (${AUTOMATION_TAB_KIND}). Skipping orchestration.`);
} else {
const autoState = getAutomationState();
console.log(`[AutoGrind] Current phase: ${autoState.phase}, running: ${isAutomationRunning()}`);
if (isAutomationRunning() && autoState.phase !== PHASES.IDLE && autoState.phase !== PHASES.COMPLETE) {
// Active automation in progress - wait for DOM and dispatch
waitForRewardPageReady(() => {
dispatchPhase(getAutomationState());
});
} else {
console.log("[AutoGrind] Idle. Waiting for Start button click.");
}
}
}
/**
* Show a small ring countdown in the bottom-right corner that depletes over `timeoutMs`.
* @param {number} timeoutMs - Duration matching the auto-close delay.
*/
function showAutoCloseRing(timeoutMs) {
const size = 44;
const r = 17;
const stroke = 3.5;
const circumference = 2 * Math.PI * r;
const ring = document.createElement('div');
ring.style.cssText = [
'position:fixed', 'bottom:16px', 'right:16px', 'z-index:2147483647',
'width:' + size + 'px', 'height:' + size + 'px',
'background:rgba(15,15,15,0.82)', 'border-radius:50%',
'display:flex', 'align-items:center', 'justify-content:center',
'box-shadow:0 2px 8px rgba(0,0,0,0.5)'
].join(';');
ring.innerHTML = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="transform:rotate(-90deg)">
<circle cx="${size/2}" cy="${size/2}" r="${r}" fill="none" stroke="#333" stroke-width="${stroke}"/>
<circle id="ag-close-ring" cx="${size/2}" cy="${size/2}" r="${r}" fill="none" stroke="#60a5fa" stroke-width="${stroke}"
stroke-dasharray="${circumference}" stroke-dashoffset="0"
style="transition:stroke-dashoffset ${timeoutMs}ms linear;"/>
</svg>`;
const attach = () => {
document.body.appendChild(ring);
requestAnimationFrame(() => requestAnimationFrame(() => {
const arc = ring.querySelector('#ag-close-ring');
if (arc) arc.style.strokeDashoffset = String(circumference);
}));
};
if (document.body) attach();
else document.addEventListener('DOMContentLoaded', attach);
}
/**
* Close the current tab if it has been registered for auto-closing.
* Waits for the specified timeout before closing the tab.
* If the tab cant be closed, then use a workaround for modern browser's limitation in closing tabs that werent opened by the script.
* Tip: This workaround still might not work, so you can use an external tool to automate closing windows
* by checking for the title of the window (Close this window).
*/
// Auto-close logic: only close tabs marked by this script (ag_child=1 in hash) AND
// only while automation is actually running. If the user has clicked Stop, no tab
// closes — even ones with stale entries in tabsToClose. The marker is read from the
// URL hash so Bing's tracking servers never see it.
if (getAutoCloseTabs() && isAutomationRunning() && pageHashParams.get('ag_child') === '1') {
const currentUrl = new URL(window.location.href);
const tabToClose = tabsToClose.find(tab => {
if (window.location.href === tab.url) return true;
try {
const stored = new URL(tab.url);
return currentUrl.origin === stored.origin && currentUrl.pathname === stored.pathname;
} catch {
return false;
}
});
if (tabToClose) {
tabsToClose = tabsToClose.filter(tab => tab.url != tabToClose.url);
GM_setValue("tabsToClose", tabsToClose);
showAutoCloseRing(tabToClose.timeout);
setTimeout(() => { if (isAutomationRunning()) window.close(); }, tabToClose.timeout);
}
}
/*=============================================*\
|* CSS STYLES *|
\*=============================================*/
const stylesheet = Object.assign(document.createElement("style"), {textContent: `
/******** Design tokens *********/
:root {
--ag-font: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
--ag-accent: #E5BA41;
--ag-accent-soft: #f4cf6b;
--ag-navy: #2D3C59;
--ag-navy-deep: #1f2b42;
--ag-blue: #4C6FA7;
--ag-green: #94A378;
--ag-red: #D14C4C;
--ag-orange: #D1855C;
--ag-radius: 14px;
--ag-radius-lg: 18px;
--ag-radius-pill: 999px;
--ag-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.08);
--ag-shadow-md: 0 4px 10px rgba(15, 23, 42, 0.08), 0 10px 24px rgba(15, 23, 42, 0.10);
--ag-shadow-lg: 0 12px 28px rgba(15, 23, 42, 0.18), 0 24px 56px rgba(15, 23, 42, 0.22);
--ag-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
/******** Modern Button Container *********/
.auto-search-container {
position: fixed;
top: 90px;
left: 20px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
z-index: 9998;
font-family: var(--ag-font);
}
.modern-button {
display: flex;
align-items: center;
gap: 10px;
overflow: hidden;
cursor: pointer;
border-radius: var(--ag-radius);
background: linear-gradient(135deg, var(--ag-accent-soft) 0%, var(--ag-accent) 100%);
padding: 10px 18px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.35) inset,
0 -1px 0 rgba(0, 0, 0, 0.08) inset,
0 6px 16px rgba(229, 186, 65, 0.28),
0 2px 4px rgba(15, 23, 42, 0.08);
transition: transform 0.35s var(--ag-ease), box-shadow 0.35s var(--ag-ease), filter 0.35s var(--ag-ease);
color: #fff;
font-family: var(--ag-font);
font-weight: 600;
font-size: 13.5px;
letter-spacing: 0.1px;
min-width: 48px;
position: relative;
will-change: transform;
}
.modern-button:hover {
transform: translateX(6px);
filter: brightness(1.04);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.4) inset,
0 -1px 0 rgba(0, 0, 0, 0.08) inset,
0 10px 22px rgba(229, 186, 65, 0.38),
0 4px 8px rgba(15, 23, 42, 0.12);
}
.modern-button:active {
transform: translateX(6px) scale(0.98);
filter: brightness(0.98);
}
.button-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
transition: transform 0.35s var(--ag-ease);
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.15));
}
.modern-button:hover .button-icon {
transform: scale(1.08) rotate(4deg);
}
.modern-button span {
white-space: nowrap;
transition: opacity 0.3s ease;
}
/* Settings button styling */
.settings-icon.modern-button {
background: linear-gradient(135deg, #3a4d70 0%, var(--ag-navy) 100%);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.12) inset,
0 -1px 0 rgba(0, 0, 0, 0.2) inset,
0 6px 16px rgba(45, 60, 89, 0.32),
0 2px 4px rgba(15, 23, 42, 0.1);
}
.settings-icon.modern-button:hover {
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.14) inset,
0 -1px 0 rgba(0, 0, 0, 0.22) inset,
0 10px 22px rgba(45, 60, 89, 0.42),
0 4px 8px rgba(15, 23, 42, 0.14);
}
/* Queue button styling */
.queue-icon.modern-button {
background: linear-gradient(135deg, #5d83c3 0%, var(--ag-blue) 100%);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.18) inset,
0 -1px 0 rgba(0, 0, 0, 0.16) inset,
0 6px 16px rgba(76, 111, 167, 0.32),
0 2px 4px rgba(15, 23, 42, 0.1);
}
.queue-icon.modern-button:hover {
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.2) inset,
0 -1px 0 rgba(0, 0, 0, 0.18) inset,
0 10px 22px rgba(76, 111, 167, 0.45),
0 4px 8px rgba(15, 23, 42, 0.14);
}
/* Mini button row + size override for low-emphasis actions */
.mini-button-row {
display: flex;
gap: 6px;
margin-top: 2px;
padding-left: 6px;
}
.mini-button.modern-button {
min-width: 0;
padding: 7px;
border-radius: 10px;
opacity: 0.7;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.18) inset,
0 -1px 0 rgba(0, 0, 0, 0.1) inset,
0 3px 8px rgba(15, 23, 42, 0.12);
}
.mini-button.modern-button:hover {
opacity: 1;
transform: translateX(3px);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.22) inset,
0 -1px 0 rgba(0, 0, 0, 0.12) inset,
0 6px 14px rgba(15, 23, 42, 0.18);
}
.mini-button.modern-button:active {
transform: translateX(3px) scale(0.96);
}
.mini-button .button-icon {
width: 14px;
height: 14px;
}
.mini-button.modern-button:hover .button-icon {
transform: scale(1.08) rotate(4deg);
}
/* Rate / Report button variants — quieter, neutral surface */
.rate-icon.modern-button,
.report-icon.modern-button {
background: rgba(45, 60, 89, 0.55);
color: #fff;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.rate-icon.modern-button:hover {
background: linear-gradient(135deg, #b1bf94 0%, var(--ag-green) 100%);
}
.report-icon.modern-button:hover {
background: linear-gradient(135deg, #e3a47e 0%, var(--ag-orange) 100%);
}
/* Feedback dialog (Rate / Report) */
.feedback-dialog-overlay {
position: fixed;
inset: 0;
background: radial-gradient(ellipse at top, rgba(15, 23, 42, 0.55), rgba(0, 0, 0, 0.7));
backdrop-filter: blur(14px) saturate(140%);
-webkit-backdrop-filter: blur(14px) saturate(140%);
display: none;
justify-content: center;
align-items: center;
z-index: 10003;
animation: fadeIn 0.3s ease;
font-family: var(--ag-font);
}
.feedback-dialog {
width: 90%;
max-width: 420px;
background: linear-gradient(180deg, #344361 0%, #2D3C59 55%, #1f2b42 100%);
border-radius: 22px;
padding: 28px 26px 22px;
box-shadow:
0 30px 70px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(255, 255, 255, 0.06);
animation: slideUp 0.4s var(--ag-ease);
text-align: center;
color: #fff;
}
.feedback-dialog-icon {
width: 60px;
height: 60px;
margin: 0 auto 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.22) inset,
0 -1px 0 rgba(0, 0, 0, 0.18) inset,
0 12px 28px rgba(0, 0, 0, 0.3);
}
.feedback-dialog-icon svg {
width: 30px;
height: 30px;
}
.feedback-dialog-icon.rate {
background: linear-gradient(135deg, var(--ag-accent-soft), var(--ag-accent));
}
.feedback-dialog-icon.report {
background: linear-gradient(135deg, #e3a47e, var(--ag-orange));
}
.feedback-dialog h3 {
margin: 0 0 10px;
font-family: var(--ag-font);
font-size: 19px;
font-weight: 700;
letter-spacing: 0.1px;
color: #fff;
}
.feedback-dialog p {
margin: 0 0 22px;
font-family: var(--ag-font);
font-size: 13.5px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.78);
}
.feedback-dialog-actions {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.feedback-btn {
border: none;
cursor: pointer;
padding: 10px 20px;
border-radius: var(--ag-radius-pill);
font-family: var(--ag-font);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.2px;
transition: transform 0.2s var(--ag-ease), filter 0.2s var(--ag-ease), box-shadow 0.2s var(--ag-ease), background 0.2s var(--ag-ease);
}
.feedback-btn.secondary {
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.feedback-btn.secondary:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
}
.feedback-btn.primary {
color: #1f2b42;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.3) inset,
0 6px 16px rgba(0, 0, 0, 0.25);
}
.feedback-btn.primary.rate {
background: linear-gradient(135deg, var(--ag-accent-soft), var(--ag-accent));
}
.feedback-btn.primary.report {
background: linear-gradient(135deg, #e3a47e, var(--ag-orange));
color: #fff;
}
.feedback-btn.primary:hover {
transform: translateY(-1px);
filter: brightness(1.05);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.32) inset,
0 10px 22px rgba(0, 0, 0, 0.3);
}
.feedback-btn.primary:active {
transform: translateY(0);
filter: brightness(0.98);
}
/* Script notification */
.script-notification {
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #3a4d70 0%, var(--ag-navy) 100%);
color: white;
padding: 14px 22px;
border-radius: var(--ag-radius);
box-shadow: var(--ag-shadow-lg);
font-family: var(--ag-font);
font-weight: 600;
font-size: 13.5px;
letter-spacing: 0.1px;
z-index: 10001;
animation: slideInRight 0.35s var(--ag-ease), fadeOut 0.3s ease 2.7s;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Dark mode support */
.b_drk .modern-button,
.b_dark .modern-button {
background: #E5BA41;
box-shadow: 0 4px 12px rgba(229, 186, 65, 0.4);
}
.b_drk .settings-icon.modern-button,
.b_dark .settings-icon.modern-button {
background: #2D3C59;
box-shadow: 0 4px 12px rgba(45, 60, 89, 0.4);
}
.b_drk .queue-icon.modern-button,
.b_dark .queue-icon.modern-button {
background: #4C6FA7;
box-shadow: 0 4px 12px rgba(76, 111, 167, 0.45);
}
/* Start/Stop toggle: when automation is running, recolor to a stop-red */
.search-icon.running {
background: linear-gradient(135deg, #e26a6a 0%, var(--ag-red) 100%) !important;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.22) inset,
0 -1px 0 rgba(0, 0, 0, 0.16) inset,
0 6px 16px rgba(209, 76, 76, 0.36),
0 2px 4px rgba(15, 23, 42, 0.1);
}
.search-icon.running:hover {
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.24) inset,
0 -1px 0 rgba(0, 0, 0, 0.18) inset,
0 10px 22px rgba(209, 76, 76, 0.5),
0 4px 8px rgba(15, 23, 42, 0.14);
}
/* Queue Monitor stage banner shown while planning/executing */
.queue-stage-banner {
font-size: 11.5px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, #5d83c3 0%, var(--ag-blue) 100%);
padding: 8px 12px;
border-radius: 10px;
margin-bottom: 10px;
text-align: center;
letter-spacing: 0.4px;
text-transform: uppercase;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.15) inset,
0 -1px 0 rgba(0, 0, 0, 0.12) inset,
0 4px 10px rgba(76, 111, 167, 0.28);
}
/* Search states */
.search-icon.searching {
background: linear-gradient(135deg, var(--ag-accent-soft) 0%, var(--ag-accent) 100%) !important;
animation: shimmer 2.2s infinite ease-in-out;
}
@keyframes shimmer {
0%, 100% {
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.35) inset,
0 6px 16px rgba(229, 186, 65, 0.32);
}
50% {
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.45) inset,
0 10px 26px rgba(229, 186, 65, 0.55);
}
}
.search-icon.counting {
background: linear-gradient(135deg, #b1bf94 0%, var(--ag-green) 100%) !important;
animation: pulse-counting 1.2s infinite ease-in-out;
}
@keyframes pulse-counting {
0%, 100% {
transform: translateX(6px) scale(1);
}
50% {
transform: translateX(6px) scale(1.04);
}
}
/******** Settings Overlay *********/
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
transform: translateY(24px) scale(0.98);
opacity: 0;
}
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes slideDown {
from {
transform: translateY(0) scale(1);
opacity: 1;
}
to {
transform: translateY(24px) scale(0.98);
opacity: 0;
}
}
.settings-overlay {
transition: opacity 0.3s ease;
font-family: var(--ag-font);
}
.settings-content {
width: 90%;
max-width: 680px;
font-family: var(--ag-font);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 22px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0));
}
.settings-tabs {
display: flex;
gap: 6px;
padding: 10px 14px;
background: var(--ag-navy-deep);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.settings-tab {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
padding: 6px 14px;
border-radius: var(--ag-radius-pill);
font-family: var(--ag-font);
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.2px;
cursor: pointer;
transition: background 0.25s var(--ag-ease), color 0.25s var(--ag-ease), border-color 0.25s var(--ag-ease);
}
.settings-tab:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.settings-tab.active {
background: linear-gradient(135deg, var(--ag-accent-soft) 0%, var(--ag-accent) 100%);
border-color: transparent;
color: var(--ag-navy);
box-shadow: 0 4px 10px rgba(229, 186, 65, 0.28);
}
.settings-close-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s var(--ag-ease), transform 0.3s var(--ag-ease), border-color 0.3s var(--ag-ease);
}
.settings-close-btn:hover {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.22);
transform: rotate(90deg);
}
.settings-close-btn svg {
width: 18px;
height: 18px;
opacity: 0.9;
}
.settings-body {
scrollbar-width: thin;
scrollbar-color: rgba(45, 60, 89, 0.25) transparent;
}
.settings-body::-webkit-scrollbar {
width: 8px;
}
.settings-body::-webkit-scrollbar-track {
background: transparent;
}
.settings-body::-webkit-scrollbar-thumb {
background: rgba(45, 60, 89, 0.22);
border-radius: 10px;
border: 2px solid transparent;
background-clip: padding-box;
}
.settings-body::-webkit-scrollbar-thumb:hover {
background: rgba(45, 60, 89, 0.45);
background-clip: padding-box;
}
.settings-item {
margin-bottom: 10px;
}
.queue-summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin-bottom: 10px;
}
.queue-summary-card {
background: #fff;
border: 1px solid rgba(45, 60, 89, 0.08);
border-radius: 10px;
padding: 7px 10px;
display: flex;
justify-content: space-between;
font-family: var(--ag-font);
font-size: 11px;
font-weight: 600;
color: var(--ag-navy);
box-shadow: var(--ag-shadow-sm);
}
.queue-summary-card span {
color: var(--ag-blue);
font-variant-numeric: tabular-nums;
}
.queue-section {
margin-bottom: 10px;
}
.queue-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.queue-section-header h3 {
margin: 0;
font-family: var(--ag-font);
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.6px;
text-transform: uppercase;
color: rgba(45, 60, 89, 0.65);
}
.queue-count {
font-size: 10.5px;
font-weight: 600;
color: #94a3b8;
font-variant-numeric: tabular-nums;
}
.queue-list {
display: flex;
flex-direction: column;
gap: 5px;
}
.queue-row {
background: #fff;
border: 1px solid rgba(45, 60, 89, 0.08);
border-radius: 10px;
padding: 7px 10px;
box-shadow: var(--ag-shadow-sm);
transition: transform 0.2s var(--ag-ease), box-shadow 0.2s var(--ag-ease);
}
.queue-row:hover {
transform: translateY(-1px);
box-shadow: var(--ag-shadow-md);
}
.queue-row.small {
padding: 6px 10px;
}
.queue-row-title {
font-family: var(--ag-font);
font-size: 11.5px;
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
}
.queue-row-meta {
display: flex;
gap: 6px;
align-items: center;
}
.queue-type,
.queue-status {
font-family: var(--ag-font);
font-size: 9.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
padding: 2px 8px;
border-radius: var(--ag-radius-pill);
display: inline-flex;
align-items: center;
gap: 4px;
}
.queue-type {
background: rgba(76, 111, 167, 0.1);
color: var(--ag-blue);
}
.queue-status.status-pending {
background: rgba(229, 186, 65, 0.18);
color: #a06d00;
}
.queue-status.status-progress {
background: rgba(76, 111, 167, 0.16);
color: #1d4ed8;
}
.queue-status.status-progress::before {
content: '';
width: 6px; height: 6px; border-radius: 50%;
background: #1d4ed8;
animation: ag-pulse-dot 1.2s infinite ease-in-out;
}
@keyframes ag-pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.85); }
}
.queue-status.status-complete {
background: rgba(148, 163, 120, 0.2);
color: #2f6f31;
}
.queue-status.status-failed {
background: rgba(209, 76, 76, 0.16);
color: #b91c1c;
}
.queue-clear-btn {
background: linear-gradient(135deg, #e3a47e 0%, var(--ag-orange) 100%);
color: #fff;
border: none;
border-radius: var(--ag-radius-pill);
padding: 5px 12px;
font-family: var(--ag-font);
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.3px;
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 4px 10px rgba(209, 133, 92, 0.32);
transition: transform 0.2s var(--ag-ease), box-shadow 0.2s var(--ag-ease), filter 0.2s var(--ag-ease);
}
.queue-clear-btn:hover {
transform: translateY(-1px);
filter: brightness(1.05);
box-shadow: 0 6px 14px rgba(209, 133, 92, 0.4);
}
.queue-empty {
font-family: var(--ag-font);
font-size: 11px;
color: #94a3b8;
background: rgba(148, 163, 184, 0.06);
border: 1px dashed rgba(148, 163, 184, 0.4);
border-radius: 10px;
padding: 10px 12px;
text-align: center;
}
.queue-lease-row {
background: rgba(76, 111, 167, 0.07);
border: 1px solid rgba(76, 111, 167, 0.15);
border-radius: 10px;
padding: 6px 10px;
font-family: var(--ag-font);
font-size: 11px;
font-weight: 500;
color: var(--ag-navy);
margin-bottom: 6px;
}
.queue-lease-row strong {
color: var(--ag-blue);
font-weight: 700;
}
.queue-log-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.queue-log-row {
background: #fff;
border: 1px solid rgba(45, 60, 89, 0.08);
border-radius: 8px;
padding: 5px 9px;
font-family: var(--ag-font);
font-size: 11px;
color: #334155;
line-height: 1.4;
box-shadow: var(--ag-shadow-sm);
}
.queue-log-row span {
font-weight: 600;
color: #94a3b8;
margin-right: 6px;
font-variant-numeric: tabular-nums;
}
.queue-log-row.warn {
border-left: 3px solid var(--ag-orange);
}
.queue-log-row.error {
border-left: 3px solid var(--ag-red);
}
.queue-log-row.unmatched {
border-left: 3px solid #7c3aed;
background: linear-gradient(90deg, rgba(124, 58, 237, 0.06), rgba(124, 58, 237, 0));
color: #4c1d95;
}
.setting-card {
background: #fff;
border-radius: var(--ag-radius);
padding: 14px 16px;
box-shadow: var(--ag-shadow-sm);
transition: transform 0.3s var(--ag-ease), box-shadow 0.3s var(--ag-ease), border-color 0.3s var(--ag-ease);
border: 1px solid rgba(45, 60, 89, 0.06);
}
.setting-card:hover {
box-shadow: var(--ag-shadow-md);
border-color: rgba(229, 186, 65, 0.35);
transform: translateY(-1px);
}
.setting-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.settings-item-name {
font-family: var(--ag-font);
font-size: 13.5px;
font-weight: 600;
color: #1f2937;
letter-spacing: 0.1px;
}
.settings-item-value {
font-family: var(--ag-font);
font-size: 11.5px;
font-weight: 700;
color: var(--ag-navy);
background: linear-gradient(135deg, rgba(229, 186, 65, 0.22), rgba(229, 186, 65, 0.12));
padding: 4px 12px;
border-radius: var(--ag-radius-pill);
min-width: 50px;
text-align: center;
letter-spacing: 0.3px;
font-variant-numeric: tabular-nums;
box-shadow: inset 0 0 0 1px rgba(229, 186, 65, 0.25);
}
.settings-item-input {
margin-bottom: 10px;
}
.settings-item-description {
font-family: var(--ag-font);
font-size: 11.5px;
color: #64748b;
line-height: 1.55;
padding-top: 10px;
border-top: 1px solid rgba(45, 60, 89, 0.08);
}
/* Modern Slider */
.modern-slider {
width: 100%;
height: 6px;
border-radius: 999px;
background: linear-gradient(90deg, var(--ag-green) 0%, var(--ag-green) 100%);
background-color: rgba(45, 60, 89, 0.08);
outline: none;
-webkit-appearance: none;
cursor: pointer;
}
.modern-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow:
0 0 0 1px rgba(45, 60, 89, 0.12),
0 4px 10px rgba(148, 163, 120, 0.45);
cursor: pointer;
transition: transform 0.25s var(--ag-ease), box-shadow 0.25s var(--ag-ease);
}
.modern-slider::-webkit-slider-thumb:hover {
transform: scale(1.12);
box-shadow:
0 0 0 1px rgba(45, 60, 89, 0.15),
0 6px 14px rgba(148, 163, 120, 0.6);
}
.modern-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow:
0 0 0 1px rgba(45, 60, 89, 0.12),
0 4px 10px rgba(148, 163, 120, 0.45);
cursor: pointer;
border: none;
transition: transform 0.25s var(--ag-ease), box-shadow 0.25s var(--ag-ease);
}
.modern-slider::-moz-range-thumb:hover {
transform: scale(1.12);
box-shadow:
0 0 0 1px rgba(45, 60, 89, 0.15),
0 6px 14px rgba(148, 163, 120, 0.6);
}
.modern-slider:disabled {
opacity: 0.45;
cursor: not-allowed;
}
/* Modern Checkbox */
.modern-checkbox {
width: 44px;
height: 24px;
position: relative;
-webkit-appearance: none;
background: rgba(45, 60, 89, 0.18);
outline: none;
border-radius: 999px;
cursor: pointer;
transition: background 0.3s var(--ag-ease);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08);
}
.modern-checkbox:checked {
background: linear-gradient(135deg, #b1bf94 0%, var(--ag-green) 100%);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.modern-checkbox::before {
content: '';
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
top: 3px;
left: 3px;
background: #fff;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(0, 0, 0, 0.04);
transition: left 0.3s var(--ag-ease);
}
.modern-checkbox:checked::before {
left: 23px;
}
/* Range Slider Container */
.range-slider {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Dark mode support for settings */
.b_drk .setting-card,
.b_dark .setting-card {
background: #1f2937;
border-color: rgba(255, 255, 255, 0.05);
}
.b_drk .setting-card:hover,
.b_dark .setting-card:hover {
border-color: rgba(229, 186, 65, 0.35);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}
.b_drk .settings-item-name,
.b_dark .settings-item-name {
color: #f1f5f9;
}
.b_drk .settings-item-description,
.b_dark .settings-item-description {
color: #94a3b8;
border-top-color: rgba(255, 255, 255, 0.08);
}
.b_drk .settings-item-value,
.b_dark .settings-item-value {
color: #f8fafc;
background: linear-gradient(135deg, rgba(229, 186, 65, 0.28), rgba(229, 186, 65, 0.16));
box-shadow: inset 0 0 0 1px rgba(229, 186, 65, 0.3);
}
.b_drk .settings-body,
.b_dark .settings-body {
background-color: #0f172a !important;
}
.b_drk .queue-row,
.b_drk .queue-summary-card,
.b_drk .queue-log-row,
.b_dark .queue-row,
.b_dark .queue-summary-card,
.b_dark .queue-log-row {
background: #1f2937;
border-color: rgba(255, 255, 255, 0.06);
color: #e2e8f0;
}
.b_drk .queue-summary-card,
.b_dark .queue-summary-card {
color: #cbd5e1;
}
.b_drk .queue-summary-card span,
.b_dark .queue-summary-card span {
color: var(--ag-accent-soft);
}
.b_drk .queue-row-title,
.b_dark .queue-row-title {
color: #f1f5f9;
}
.b_drk .queue-lease-row,
.b_dark .queue-lease-row {
background: rgba(76, 111, 167, 0.14);
border-color: rgba(76, 111, 167, 0.28);
color: #cbd5e1;
}
.b_drk .queue-lease-row strong,
.b_dark .queue-lease-row strong {
color: var(--ag-accent-soft);
}
.b_drk .queue-empty,
.b_dark .queue-empty {
background: rgba(148, 163, 184, 0.06);
border-color: rgba(148, 163, 184, 0.18);
color: #94a3b8;
}
.b_drk .queue-section-header h3,
.b_dark .queue-section-header h3 {
color: rgba(241, 245, 249, 0.65);
}
.b_drk .queue-log-row,
.b_dark .queue-log-row {
color: #cbd5e1;
}
.b_drk .queue-log-row span,
.b_dark .queue-log-row span {
color: #64748b;
}
.b_drk .queue-log-row.unmatched,
.b_dark .queue-log-row.unmatched {
background: linear-gradient(90deg, rgba(124, 58, 237, 0.18), rgba(124, 58, 237, 0));
border-color: rgba(159, 122, 234, 0.35);
color: #e9d8fd;
}
`})
// Append styles to all pages
document.head.appendChild(stylesheet);
} catch (e) {
console.error("[AutoGrind] ERROR:", e);
console.error("[AutoGrind] Stack trace:", e.stack);
}