Created
May 17, 2026 14:27
-
-
Save srghma/3a18ab5450012cc649d17c8769d6a345 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Djinni Quick Apply Pro | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.2.0 | |
| // @description Automate job applications on Djinni.co directly via Tampermonkey | |
| // @author You | |
| // @match https://djinni.co/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| /* ======================================================================== | |
| 1. STATE MANAGEMENT (localStorage) | |
| ======================================================================== */ | |
| const STATE_KEY = "djinni_bot_state"; | |
| const POS_KEY = "djinni_bot_pos"; | |
| const defaultState = { | |
| queue: [], | |
| salary: 3000, | |
| isRunning: false, | |
| status: "Idle", | |
| }; | |
| const Storage = { | |
| get: () => { | |
| try { | |
| const data = localStorage.getItem(STATE_KEY); | |
| return data ? JSON.parse(data) : defaultState; | |
| } catch { | |
| return defaultState; | |
| } | |
| }, | |
| set: (state) => { | |
| localStorage.setItem(STATE_KEY, JSON.stringify(state)); | |
| renderUI(); // Automatically sync UI on state change | |
| }, | |
| update: (updates) => { | |
| const current = Storage.get(); | |
| const next = { ...current, ...updates }; | |
| Storage.set(next); | |
| return next; | |
| }, | |
| }; | |
| /* ======================================================================== | |
| 2. DOM UTILITIES | |
| ======================================================================== */ | |
| const isVisible = (el) => { | |
| if (!el || !(el instanceof HTMLElement)) return false; | |
| if (el.tagName.toLowerCase() === "input" && el.type === "hidden") return false; | |
| const style = window.getComputedStyle(el); | |
| if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") { | |
| return false; | |
| } | |
| const rect = el.getBoundingClientRect(); | |
| if (rect.width === 0 || rect.height === 0) return false; | |
| return true; | |
| }; | |
| const waitForVisibleElement = async (selector, timeout = 5000) => { | |
| const start = Date.now(); | |
| while (Date.now() - start < timeout) { | |
| const el = document.querySelector(selector); | |
| if (isVisible(el)) return el; | |
| await new Promise((r) => setTimeout(r, 200)); | |
| } | |
| return null; | |
| }; | |
| /* ======================================================================== | |
| 3. BACKGROUND FETCH CONTROLLER (MACRO 2) | |
| ======================================================================== */ | |
| let isFetchingQueue = false; | |
| const processFetchQueue = async () => { | |
| if (isFetchingQueue) return; | |
| isFetchingQueue = true; | |
| while (true) { | |
| const state = Storage.get(); | |
| const itemToFetch = state.queue.find((q) => q.fetchStatus === "pending"); | |
| if (!itemToFetch) break; // Finished queue | |
| // Update UI to show fetching | |
| Storage.update({ | |
| queue: state.queue.map((q) => | |
| q.id === itemToFetch.id ? { ...q, fetchStatus: "fetching..." } : q | |
| ), | |
| }); | |
| try { | |
| const res = await fetch(itemToFetch.url); | |
| const html = await res.text(); | |
| // Check for specific Djinni statuses | |
| const alreadyApplied = html.includes("You've applied to this job already"); | |
| const hasConversation = html.includes("You have a conversation with this recruiter already"); | |
| const canApply = html.includes("js-inbox-toggle-reply-form") || html.includes("Apply for the job"); | |
| const countryIssue = | |
| html.includes("from your location") || | |
| html.includes("from your country") || | |
| html.toLowerCase().includes("does not hire") || | |
| html.toLowerCase().includes("doesn't hire"); | |
| let status = "new"; | |
| let shouldKeepChecked = canApply && !alreadyApplied; | |
| if (alreadyApplied) { | |
| status = "cannot_apply_you_already_applied"; | |
| } else if (hasConversation) { | |
| if (canApply) { | |
| status = "have_conversation_already_but_job_is_new_and_can_apply"; | |
| } else if (countryIssue) { | |
| status = "have_conversation_already_but_job_is_new_and_cannot_apply_bc_they_dont_hire_from_my_country"; | |
| } else { | |
| status = "have_conversation_already_cannot_apply_other_reason"; | |
| } | |
| } else { | |
| if (canApply) { | |
| status = "dont_have_conversation_job_is_new_and_can_apply"; | |
| } else if (countryIssue) { | |
| status = "dont_have_conversation_job_is_new_and_cannot_apply_bc_they_dont_hire_from_my_country"; | |
| } else { | |
| status = "cannot_apply_other_reason"; | |
| } | |
| } | |
| const latestState = Storage.get(); | |
| Storage.update({ | |
| queue: latestState.queue.map((q) => | |
| q.id === itemToFetch.id | |
| ? { ...q, fetchStatus: status, active: shouldKeepChecked } | |
| : q | |
| ), | |
| }); | |
| } catch (e) { | |
| console.error("[Djinni Bot] Fetch Error:", e); | |
| const latestState = Storage.get(); | |
| Storage.update({ | |
| queue: latestState.queue.map((q) => | |
| q.id === itemToFetch.id ? { ...q, fetchStatus: "error_fetching_page" } : q | |
| ), | |
| }); | |
| } | |
| // 5 Seconds delay as requested to respect rate limits | |
| await new Promise((r) => setTimeout(r, 5000)); | |
| } | |
| isFetchingQueue = false; | |
| }; | |
| /* ======================================================================== | |
| 4. AUTOMATION LOGIC (MACRO) | |
| ======================================================================== */ | |
| const finishJob = (reason, isSuccess = true) => { | |
| console.log(`[Djinni Bot] ${reason}`); | |
| const state = Storage.get(); | |
| state.queue.shift(); // Remove the current job from the queue | |
| state.status = isSuccess ? "Job applied! Moving to next..." : `Skipped: ${reason}`; | |
| Storage.set(state); | |
| setTimeout(() => { | |
| processQueue(); // proceed to next item | |
| }, 1500); | |
| }; | |
| const pauseJob = (reason) => { | |
| console.warn(`[Djinni Bot] Paused: ${reason}`); | |
| Storage.update({ isRunning: false, status: reason }); | |
| }; | |
| const runApplyMacro = async (salary) => { | |
| console.log("[Djinni Bot] Starting macro..."); | |
| Storage.update({ status: "Running automation..." }); | |
| // Let the DOM settle | |
| await new Promise((r) => setTimeout(r, 1000)); | |
| const bodyText = document.body.innerText.replace(/\s+/g, " "); | |
| const isExistingOkVisible = isVisible(document.getElementById("applied_ok")); | |
| const alreadyApplied = bodyText.includes("You've applied to this job already") || isExistingOkVisible; | |
| // 1. Hard Block: Already Applied | |
| if (alreadyApplied) { | |
| finishJob("Already applied to this specific job", false); | |
| return; | |
| } | |
| // 2. Look for Apply Button (Overrides "Existing Conversation" warning) | |
| const applyBtn = document.querySelector(".js-inbox-toggle-reply-form"); | |
| const isApplyBtnVisible = isVisible(applyBtn); | |
| if (!isApplyBtnVisible) { | |
| finishJob("Apply button not found or not permitted", false); | |
| return; | |
| } | |
| // 3. Click Apply | |
| console.log("[Djinni Bot] Found Apply button, clicking..."); | |
| applyBtn.scrollIntoView({ behavior: "smooth", block: "center" }); | |
| await new Promise((r) => setTimeout(r, 800)); | |
| applyBtn.click(); | |
| const formTextarea = await waitForVisibleElement("#message", 5000); | |
| if (!formTextarea) { | |
| pauseJob("Form did not load in time"); | |
| return; | |
| } | |
| // 4. Handle Template | |
| const textarea = document.getElementById("message"); | |
| if (textarea && textarea.tagName === "TEXTAREA" && !textarea.value.trim()) { | |
| const template = document.querySelector(".js-template-put"); | |
| if (isVisible(template)) { | |
| template.click(); | |
| let attempts = 0; | |
| while (!textarea.value.trim() && attempts < 20) { | |
| await new Promise((r) => setTimeout(r, 200)); | |
| attempts++; | |
| } | |
| } | |
| } | |
| // 5. Handle Salary | |
| const salaryBtn = document.querySelector(".js-salary-toggle-btn"); | |
| if (isVisible(salaryBtn)) salaryBtn.click(); | |
| const salaryInput = document.querySelector(".js-salary-input"); | |
| if (isVisible(salaryInput) && salaryInput.tagName === "INPUT") { | |
| const maxAttr = salaryInput.getAttribute("max"); | |
| let finalSalary = salary; | |
| if (maxAttr) { | |
| const maxVal = parseInt(maxAttr, 10); | |
| if (salary > maxVal) finalSalary = maxVal; | |
| } | |
| salaryInput.value = finalSalary.toString(); | |
| salaryInput.dispatchEvent(new Event("input", { bubbles: true })); | |
| salaryInput.dispatchEvent(new Event("change", { bubbles: true })); | |
| } | |
| await new Promise((r) => setTimeout(r, 1500)); // Allow HTMX/DOM bindings to settle | |
| // 6. Check Required Fields | |
| const allRequiredElements = document.querySelectorAll("input[required], textarea[required], select[required]"); | |
| const missingField = Array.from(allRequiredElements).find((el) => { | |
| if (!isVisible(el)) return false; | |
| if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") { | |
| if (el.type === "checkbox" || el.type === "radio") return !el.checked; | |
| return !el.value.trim(); | |
| } | |
| if (el.tagName === "SELECT") return !el.value.trim(); | |
| return false; | |
| }); | |
| if (missingField) { | |
| console.warn("[Djinni Bot] Missing field:", missingField); | |
| pauseJob("Please fill missing required fields"); | |
| return; | |
| } | |
| // 7. Submit | |
| const submitBtn = document.getElementById("job_apply"); | |
| if (isVisible(submitBtn)) { | |
| submitBtn.scrollIntoView({ behavior: "smooth", block: "center" }); | |
| await new Promise((r) => setTimeout(r, 500)); | |
| submitBtn.click(); | |
| } else { | |
| pauseJob("Submit button not visible"); | |
| return; | |
| } | |
| // 8. Observe for Success | |
| let timeSpentWaiting = 0; | |
| const checkSuccessInterval = setInterval(() => { | |
| timeSpentWaiting += 500; | |
| const isOkVisible = isVisible(document.getElementById("applied_ok")); | |
| const isToastVisible = isVisible(document.getElementById("candidate-notification")); | |
| if (isOkVisible || isToastVisible) { | |
| clearInterval(checkSuccessInterval); | |
| finishJob("Successfully Applied", true); | |
| } | |
| if (timeSpentWaiting >= 15000) { | |
| clearInterval(checkSuccessInterval); | |
| pauseJob("Timeout waiting for success message"); | |
| } | |
| }, 500); | |
| }; | |
| /* ======================================================================== | |
| 5. QUEUE CONTROLLER | |
| ======================================================================== */ | |
| const processQueue = () => { | |
| const state = Storage.get(); | |
| if (!state.isRunning) return; | |
| if (state.queue.length === 0) { | |
| Storage.update({ isRunning: false, status: "Queue finished." }); | |
| return; | |
| } | |
| const currentJob = state.queue[0]; | |
| if (!currentJob.active) { | |
| finishJob("Job unchecked/invalid, skipping...", false); | |
| return; | |
| } | |
| const match = window.location.href.match(/\/jobs\/(\d+)/); | |
| const currentIdOnPage = match ? match[1] : null; | |
| if (currentIdOnPage === currentJob.id) { | |
| setTimeout(() => runApplyMacro(state.salary), 1000); | |
| } else { | |
| Storage.update({ status: `Loading ${currentJob.title.substring(0, 15)}...` }); | |
| window.location.href = currentJob.url; | |
| } | |
| }; | |
| /* ======================================================================== | |
| 6. UI INJECTION & RENDER LOOP | |
| ======================================================================== */ | |
| const injectStyles = () => { | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| #dj-bot-widget { | |
| position: fixed; width: 340px; background: white; border: 2px solid #2563eb; | |
| border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); z-index: 999999; | |
| font-family: ui-sans-serif, system-ui, sans-serif; color: #1f2937; | |
| } | |
| .dj-drag-handle { | |
| background: #eff6ff; color: #2563eb; font-size: 10px; font-weight: bold; | |
| text-align: center; padding: 6px; cursor: move; user-select: none; | |
| border-top-left-radius: 6px; border-top-right-radius: 6px; border-bottom: 1px solid #bfdbfe; | |
| } | |
| .dj-content { padding: 12px; } | |
| .dj-input-group { margin-bottom: 12px; } | |
| .dj-input-group label { display: block; font-size: 12px; font-weight: bold; margin-bottom: 4px; } | |
| .dj-input-group input { width: 100%; padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box; } | |
| .dj-queue-container { max-height: 200px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 4px; padding: 6px; margin-bottom: 12px; } | |
| .dj-queue-header { font-size: 10px; font-weight: bold; text-transform: uppercase; color: #6b7280; margin-bottom: 6px; } | |
| .dj-queue-item { display: flex; align-items: center; gap: 8px; padding: 6px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 6px; background: white; flex-wrap: wrap; } | |
| .dj-queue-item.dj-active-item { background: #eff6ff; border-color: #93c5fd; } | |
| .dj-queue-item a { font-size: 10px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #1d4ed8; text-decoration: none; min-width: 150px; } | |
| .dj-queue-item a:hover { text-decoration: underline; } | |
| .dj-queue-item button { background: none; border: none; color: #ef4444; cursor: pointer; padding: 2px; font-size: 14px; } | |
| .dj-queue-item button:hover { background: #fee2e2; border-radius: 4px; } | |
| .dj-actions { display: flex; gap: 8px; } | |
| .dj-btn { flex: 1; padding: 8px; border: none; border-radius: 4px; font-weight: bold; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px;} | |
| .dj-btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .dj-btn-green { background: #16a34a; color: white; } | |
| .dj-btn-green:hover { background: #15803d; } | |
| .dj-btn-yellow { background: #eab308; color: white; } | |
| .dj-btn-yellow:hover { background: #ca8a04; } | |
| .dj-btn-gray { background: #e5e7eb; color: #4b5563; flex: 0.3; } | |
| .dj-btn-gray:hover { background: #d1d5db; } | |
| .dj-btn-blue { background: #2563eb; color: white; width: 100%; margin-top: 8px; } | |
| .dj-btn-blue:hover { background: #1d4ed8; } | |
| .dj-status { margin-top: 10px; font-size: 10px; text-align: center; font-weight: 500; color: #6b7280; } | |
| .dj-status span { color: #2563eb; } | |
| .helper-cb { margin-right: 12px; transform: scale(1.5); accent-color: #2563eb; cursor: pointer; } | |
| `; | |
| document.head.appendChild(style); | |
| }; | |
| const createWidget = () => { | |
| const isJobPage = /^\/jobs\/\d+/.test(window.location.pathname); | |
| const widget = document.createElement("div"); | |
| widget.id = "dj-bot-widget"; | |
| widget.innerHTML = ` | |
| <div class="dj-drag-handle" id="dj-drag-handle">≡ DRAG TO REPOSITION ≡</div> | |
| <div class="dj-content"> | |
| <div class="dj-input-group"> | |
| <label>Desired Salary ($)</label> | |
| <input type="number" id="dj-salary" /> | |
| </div> | |
| <div class="dj-queue-container"> | |
| <div class="dj-queue-header">Queue (<span id="dj-q-count">0</span>)</div> | |
| <div id="dj-queue-list"></div> | |
| </div> | |
| <div class="dj-actions"> | |
| <button id="dj-btn-run" class="dj-btn dj-btn-green">▶ RUN</button> | |
| <button id="dj-btn-pause" class="dj-btn dj-btn-yellow" style="display:none">⏸ PAUSE</button> | |
| <button id="dj-btn-clear" class="dj-btn dj-btn-gray" title="Clear Queue">⏹</button> | |
| </div> | |
| ${isJobPage ? `<button id="dj-btn-apply-current" class="dj-btn dj-btn-blue">⚡ Apply for Current Job</button>` : ""} | |
| <div class="dj-status">Status: <span id="dj-status-text">Idle</span></div> | |
| </div> | |
| `; | |
| document.body.appendChild(widget); | |
| // Make Draggable | |
| const handle = document.getElementById("dj-drag-handle"); | |
| let isDragging = false, startX, startY, initialX, initialY; | |
| try { | |
| const savedPos = JSON.parse(localStorage.getItem(POS_KEY)); | |
| if (savedPos) { | |
| widget.style.left = savedPos.left; | |
| widget.style.top = savedPos.top; | |
| widget.style.bottom = "auto"; | |
| } else { | |
| widget.style.left = "20px"; | |
| widget.style.bottom = "20px"; | |
| } | |
| } catch { | |
| widget.style.left = "20px"; | |
| widget.style.bottom = "20px"; | |
| } | |
| handle.addEventListener("mousedown", (e) => { | |
| isDragging = true; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| initialX = widget.offsetLeft; | |
| initialY = widget.offsetTop; | |
| document.body.style.userSelect = "none"; | |
| }); | |
| document.addEventListener("mousemove", (e) => { | |
| if (!isDragging) return; | |
| widget.style.left = `${initialX + (e.clientX - startX)}px`; | |
| widget.style.top = `${initialY + (e.clientY - startY)}px`; | |
| widget.style.bottom = "auto"; | |
| }); | |
| document.addEventListener("mouseup", () => { | |
| if (isDragging) { | |
| isDragging = false; | |
| document.body.style.userSelect = ""; | |
| localStorage.setItem(POS_KEY, JSON.stringify({ left: widget.style.left, top: widget.style.top })); | |
| } | |
| }); | |
| document.getElementById("dj-salary").addEventListener("change", (e) => Storage.update({ salary: parseInt(e.target.value) || 0 })); | |
| document.getElementById("dj-btn-run").addEventListener("click", () => { | |
| Storage.update({ isRunning: true }); | |
| processQueue(); | |
| }); | |
| document.getElementById("dj-btn-pause").addEventListener("click", () => Storage.update({ isRunning: false, status: "Paused manually" })); | |
| document.getElementById("dj-btn-clear").addEventListener("click", () => Storage.update({ isRunning: false, queue: [], status: "Queue cleared" })); | |
| if (isJobPage) { | |
| document.getElementById("dj-btn-apply-current").addEventListener("click", () => { | |
| const url = window.location.href; | |
| const title = document.querySelector("h1")?.innerText || "Current Job"; | |
| const match = url.match(/\/jobs\/(\d+)/); | |
| const jobId = match ? match[1] : null; | |
| if (jobId) { | |
| const state = Storage.get(); | |
| const filteredQ = state.queue.filter((q) => q.id !== jobId); | |
| filteredQ.unshift({ id: jobId, url, title, active: true, fetchStatus: "pending" }); | |
| Storage.update({ queue: filteredQ, isRunning: true }); | |
| setTimeout(processFetchQueue, 500); // Trigger background fetch logic | |
| processQueue(); | |
| } | |
| }); | |
| } | |
| renderUI(); | |
| }; | |
| const renderUI = () => { | |
| const state = Storage.get(); | |
| const salaryInput = document.getElementById("dj-salary"); | |
| if (salaryInput && document.activeElement !== salaryInput) { | |
| salaryInput.value = state.salary; | |
| salaryInput.disabled = state.isRunning; | |
| } | |
| const qCount = document.getElementById("dj-q-count"); | |
| if (qCount) qCount.innerText = state.queue.length; | |
| const listContainer = document.getElementById("dj-queue-list"); | |
| if (listContainer) { | |
| if (state.queue.length === 0) { | |
| listContainer.innerHTML = `<div style="font-size:10px; color:#9ca3af; padding:4px;">Select jobs on the list...</div>`; | |
| } else { | |
| listContainer.innerHTML = state.queue.map((item, idx) => ` | |
| <div class="dj-queue-item ${idx === 0 ? "dj-active-item" : ""}"> | |
| <div style="display: flex; align-items: center; width: 100%; gap: 8px;"> | |
| <input type="checkbox" class="dj-q-cb" data-id="${item.id}" ${item.active ? "checked" : ""}> | |
| <a href="${item.url}" target="_blank" title="${item.title}">${item.title}</a> | |
| <button class="dj-q-del" data-id="${item.id}">✖</button> | |
| </div> | |
| ${item.fetchStatus ? ` | |
| <div style="flex-basis: 100%; font-size: 9px; color: ${item.fetchStatus.includes('cannot_apply') ? '#ef4444' : (item.fetchStatus === 'pending' || item.fetchStatus === 'fetching...' ? '#eab308' : '#16a34a')}; margin-top: 2px; padding-left: 20px; word-break: break-all;"> | |
| ${item.fetchStatus} | |
| </div>` : ''} | |
| </div> | |
| `).join(""); | |
| document.querySelectorAll(".dj-q-cb").forEach((cb) => { | |
| cb.addEventListener("change", (e) => { | |
| const id = e.target.getAttribute("data-id"); | |
| const st = Storage.get(); | |
| const newQ = st.queue.map((q) => (q.id === id ? { ...q, active: e.target.checked } : q)); | |
| Storage.update({ queue: newQ }); | |
| }); | |
| }); | |
| document.querySelectorAll(".dj-q-del").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| const id = e.target.getAttribute("data-id"); | |
| const st = Storage.get(); | |
| Storage.update({ queue: st.queue.filter((q) => q.id !== id) }); | |
| }); | |
| }); | |
| } | |
| } | |
| const btnRun = document.getElementById("dj-btn-run"); | |
| const btnPause = document.getElementById("dj-btn-pause"); | |
| if (btnRun && btnPause) { | |
| if (state.isRunning) { | |
| btnRun.style.display = "none"; | |
| btnPause.style.display = "flex"; | |
| } else { | |
| btnRun.style.display = "flex"; | |
| btnRun.disabled = state.queue.length === 0; | |
| btnPause.style.display = "none"; | |
| } | |
| } | |
| const statusText = document.getElementById("dj-status-text"); | |
| if (statusText) statusText.innerText = state.status; | |
| document.querySelectorAll(".helper-cb").forEach((cb) => { | |
| const jobId = cb.getAttribute("data-jobid"); | |
| cb.checked = state.queue.some((q) => q.id === jobId); | |
| }); | |
| }; | |
| /* ======================================================================== | |
| 7. LIST PAGE CHECKBOX INJECTION | |
| ======================================================================== */ | |
| const injectCheckboxes = () => { | |
| const items = document.querySelectorAll(".job-item:not(.helper-ready)"); | |
| const state = Storage.get(); | |
| items.forEach((item) => { | |
| item.classList.add("helper-ready"); | |
| const linkEl = item.querySelector(".job_item__header-link") || item.querySelector("a.profile"); | |
| if (!linkEl) return; | |
| const title = item.querySelector("h2")?.innerText?.trim() || item.querySelector(".job-list-item__link")?.innerText?.trim() || "Job"; | |
| const url = linkEl.href; | |
| const match = url.match(/\/jobs\/(\d+)/); | |
| const jobId = match ? match[1] : item.id.replace("job-item-", ""); | |
| const cb = document.createElement("input"); | |
| cb.type = "checkbox"; | |
| cb.className = "helper-cb"; | |
| cb.setAttribute("data-jobid", jobId); | |
| cb.checked = state.queue.some((q) => q.id === jobId); | |
| cb.addEventListener("change", (e) => { | |
| const isChecked = e.target.checked; | |
| const freshState = Storage.get(); | |
| let newQ = [...freshState.queue]; | |
| if (isChecked) { | |
| if (!newQ.some(q => q.id === jobId)) { | |
| newQ.push({ id: jobId, url, title, active: true, fetchStatus: "pending" }); | |
| setTimeout(processFetchQueue, 500); // Trigger queue handler | |
| } | |
| } else { | |
| newQ = newQ.filter((q) => q.id !== jobId); | |
| } | |
| Storage.update({ queue: newQ }); | |
| }); | |
| const targetCol = item.querySelector(".col-auto") || item.querySelector("h2")?.parentElement || item; | |
| if (targetCol) { | |
| const wrapper = document.createElement("div"); | |
| wrapper.style.display = "inline-flex"; | |
| wrapper.style.alignItems = "center"; | |
| wrapper.appendChild(cb); | |
| targetCol.prepend(wrapper); | |
| } | |
| }); | |
| }; | |
| /* ======================================================================== | |
| 8. INITIALIZATION | |
| ======================================================================== */ | |
| const init = () => { | |
| console.log("[Djinni Bot] Initializing..."); | |
| injectStyles(); | |
| createWidget(); | |
| const observer = new MutationObserver(() => injectCheckboxes()); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| injectCheckboxes(); | |
| const state = Storage.get(); | |
| // Jumpstart fetch queue if anything was left pending | |
| if (state.queue.some(q => q.fetchStatus === "pending" || q.fetchStatus === "fetching...")) { | |
| Storage.update({ queue: state.queue.map(q => q.fetchStatus === "fetching..." ? { ...q, fetchStatus: "pending" } : q) }); | |
| setTimeout(processFetchQueue, 1000); | |
| } | |
| if (state.isRunning && state.queue.length > 0) { | |
| setTimeout(() => processQueue(), 1000); | |
| } | |
| }; | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Google Maps Scraper Widget | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.1 | |
| // @description Extracts Google Maps business data precisely and allows auto-scrolling | |
| // @author You | |
| // @match https://www.google.com/maps/* | |
| // @grant GM_setClipboard | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // ========================================== | |
| // 1. Create the floating UI Widget | |
| // ========================================== | |
| const widget = document.createElement('div'); | |
| widget.style.cssText = ` | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 999999; | |
| background: white; | |
| border: 1px solid #ccc; | |
| padding: 16px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| font-family: Arial, sans-serif; | |
| font-size: 14px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| width: 260px; | |
| `; | |
| const title = document.createElement('strong'); | |
| title.innerText = '🗺️ Maps Scraper'; | |
| title.style.textAlign = 'center'; | |
| const scrollBtn = document.createElement('button'); | |
| scrollBtn.innerText = 'Scroll to End'; | |
| scrollBtn.style.cssText = 'padding: 10px; cursor: pointer; background: #1a73e8; color: white; border: none; border-radius: 4px; font-weight: bold; transition: 0.2s;'; | |
| const copyBtn = document.createElement('button'); | |
| copyBtn.innerText = 'Copy Visible Info to JSON'; | |
| copyBtn.style.cssText = 'padding: 10px; cursor: pointer; background: #34a853; color: white; border: none; border-radius: 4px; font-weight: bold;'; | |
| const statusText = document.createElement('div'); | |
| statusText.style.cssText = 'font-size: 12px; color: #555; text-align: center; font-weight: bold;'; | |
| statusText.innerText = 'Ready'; | |
| widget.appendChild(title); | |
| widget.appendChild(scrollBtn); | |
| widget.appendChild(copyBtn); | |
| widget.appendChild(statusText); | |
| document.body.appendChild(widget); | |
| // ========================================== | |
| // 2. Auto-Scrolling Logic | |
| // ========================================== | |
| let scrollInterval = null; | |
| let isScrolling = false; | |
| let lastScrollHeight = 0; | |
| let unchangedTicks = 0; | |
| function stopScrolling(reason) { | |
| clearInterval(scrollInterval); | |
| isScrolling = false; | |
| scrollBtn.innerText = 'Scroll to End'; | |
| scrollBtn.style.background = '#1a73e8'; | |
| statusText.innerText = reason; | |
| statusText.style.color = '#1a73e8'; | |
| } | |
| scrollBtn.addEventListener('click', () => { | |
| const feed = document.querySelector('div[role="feed"]'); | |
| if (!feed) { | |
| statusText.innerText = 'List not found. Search first!'; | |
| statusText.style.color = 'red'; | |
| return; | |
| } | |
| if (isScrolling) { | |
| stopScrolling('Scrolling stopped manually.'); | |
| } else { | |
| isScrolling = true; | |
| scrollBtn.innerText = 'Stop Scrolling'; | |
| scrollBtn.style.background = '#ea4335'; | |
| statusText.innerText = 'Scrolling...'; | |
| statusText.style.color = '#ea4335'; | |
| lastScrollHeight = 0; | |
| unchangedTicks = 0; | |
| scrollInterval = setInterval(() => { | |
| feed.scrollTop = feed.scrollHeight; | |
| // Detection 1: Check if Google explicitly says the list ended (.HlvSq or .PbZDve classes) | |
| const endMessage1 = document.querySelector('.HlvSq'); | |
| const endMessage2 = document.querySelector('.PbZDve'); | |
| if ((endMessage1 && endMessage1.innerText.length > 0) || (endMessage2 && endMessage2.innerText.length > 0)) { | |
| stopScrolling('Reached end of list!'); | |
| return; | |
| } | |
| // Detection 2: Check if height hasn't changed for ~2 seconds (4 ticks) | |
| if (feed.scrollHeight === lastScrollHeight) { | |
| unchangedTicks++; | |
| if (unchangedTicks >= 4) { | |
| stopScrolling('Reached end of list!'); | |
| } | |
| } else { | |
| lastScrollHeight = feed.scrollHeight; | |
| unchangedTicks = 0; // reset | |
| } | |
| }, 500); | |
| } | |
| }); | |
| // ========================================== | |
| // 3. Precise Data Extraction Logic | |
| // ========================================== | |
| copyBtn.addEventListener('click', () => { | |
| const articles = document.querySelectorAll('div[role="article"]'); | |
| if (articles.length === 0) { | |
| statusText.innerText = 'No items found on page.'; | |
| statusText.style.color = 'red'; | |
| return; | |
| } | |
| const data = Array.from(document.querySelectorAll('div[role="article"]')).map(infoContainer => { | |
| // 1. Name | |
| const nameEl = infoContainer.querySelector('.qBF1Pd'); | |
| const name = nameEl ? nameEl.textContent.trim() : null; | |
| // 2. URLs and Coordinates | |
| const mainLinkEl = infoContainer.querySelector('a.hfpxzc'); | |
| const googleMapsUrl = mainLinkEl ? mainLinkEl.href : null; | |
| let lat = null, lng = null; | |
| if (googleMapsUrl) { | |
| const latMatch = googleMapsUrl.match(/!3d([0-9.-]+)/); | |
| const lngMatch = googleMapsUrl.match(/!4d([0-9.-]+)/); | |
| if (latMatch) lat = parseFloat(latMatch[1]); | |
| if (lngMatch) lng = parseFloat(lngMatch[1]); | |
| } | |
| // 3. Rating & Reviews | |
| let rating = null; | |
| let numberOfReviews = null; | |
| const mw4etd = infoContainer.querySelector('.MW4etd'); | |
| if (mw4etd) { | |
| rating = parseFloat(mw4etd.textContent.trim().replace(',', '.')); | |
| } else { | |
| const ratingSpan = infoContainer.querySelector('span[role="img"].ZkP5Je'); | |
| if (ratingSpan) { | |
| const match = ratingSpan.getAttribute('aria-label').match(/([\d\.,]+)/); | |
| if (match) rating = parseFloat(match[1].replace(',', '.')); | |
| } | |
| } | |
| const reviewsEl = infoContainer.querySelector('.UY7F9'); | |
| if (reviewsEl) { | |
| numberOfReviews = parseInt(reviewsEl.textContent.trim().replace(/[^\d]/g, ''), 10); | |
| } | |
| // 4. Phone | |
| const phoneEl = infoContainer.querySelector('.UsdlK'); | |
| const phoneNumber = phoneEl ? phoneEl.textContent.trim() : null; | |
| // 5. Website (Language agnostic: looks for exact class/icon or data-value) | |
| let website = null; | |
| const websiteEls = infoContainer.querySelectorAll('a'); | |
| for (let a of websiteEls) { | |
| const href = a.href; | |
| const hasIcon = a.querySelector('.Cw1rxd') !== null; | |
| const hasDataValue = a.getAttribute('data-value') === 'Сайт' || a.getAttribute('data-value') === 'Website' || a.getAttribute('data-value') === 'Web site'; | |
| if (href && href.startsWith('http') && !href.includes('google.com/maps') && (hasIcon || hasDataValue)) { | |
| website = href.trim(); | |
| break; | |
| } | |
| } | |
| // 6. Address, Category, and Opening Status (DOM-based precise extraction) | |
| let category = null; | |
| let address = null; | |
| let openingStatus = null; | |
| // .W4Efsd > .W4Efsd targets the exact wrapper rows holding the strings | |
| const detailRows = infoContainer.querySelectorAll('.W4Efsd > .W4Efsd'); | |
| if (detailRows.length > 0) { | |
| // Row 1: Usually contains Category and Address | |
| const row1Spans = Array.from(detailRows[0].querySelectorAll(':scope > span')); | |
| const validTexts = []; | |
| row1Spans.forEach(span => { | |
| // Filter out the span entirely if it holds the accessibility/wheelchair icon | |
| if (span.querySelector('.google-symbols')) return; | |
| let text = span.textContent.trim().replace(/^·\s*/, '').replace(/\s*·$/, '').trim(); | |
| if (text) validTexts.push(text); | |
| }); | |
| if (validTexts.length > 0) category = validTexts[0]; | |
| if (validTexts.length > 1) address = validTexts.slice(1).join(', '); | |
| } | |
| if (detailRows.length > 1) { | |
| // Row 2: Usually contains Status and Phone Number | |
| const row2Spans = Array.from(detailRows[1].querySelectorAll(':scope > span')); | |
| const statusTexts = []; | |
| row2Spans.forEach(span => { | |
| // Filter out the span entirely if it holds the phone number (we already have it) | |
| if (span.querySelector('.UsdlK') || span.classList.contains('UsdlK')) return; | |
| let text = span.textContent.trim().replace(/^·\s*/, '').replace(/\s*·$/, '').trim(); | |
| if (text) statusTexts.push(text); | |
| }); | |
| if (statusTexts.length > 0) { | |
| openingStatus = statusTexts.join(' · '); | |
| } | |
| } | |
| return { | |
| name, | |
| rating, | |
| numberOfReviews, | |
| category, | |
| address, | |
| openingStatus, | |
| phoneNumber, | |
| website, | |
| googleMapsUrl, | |
| latitude: lat, | |
| longitude: lng | |
| }; | |
| }); | |
| // Convert the final array to JSON | |
| const jsonOutput = JSON.stringify(data, null, 2); | |
| // Copy to clipboard using Tampermonkey API | |
| GM_setClipboard(jsonOutput); | |
| // Update UI | |
| statusText.innerText = `✅ Copied ${data.length} items to clipboard!`; | |
| statusText.style.color = '#34a853'; | |
| console.log("Extracted Data:", data); | |
| }); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Indeed SmartApply Ultimate Dashboard | |
| // @namespace http://tampermonkey.net/ | |
| // @version 4.0.0 | |
| // @description Automated filling and navigation for Indeed Smart Apply. | |
| // @match https://smartapply.indeed.com/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| const WIDGET_ID = "ind-ultimate-v4"; | |
| const SETTINGS_KEY = "ind_settings_v4"; // Only stores { autoContinue: bool } | |
| const POS_KEY = "ind_dashboard_pos_v4"; | |
| // 1. DATA: Kept in code as requested | |
| const FORM_DEFAULTS = { | |
| "Current Location": "Phnom Penh", | |
| "Preferred Location": "Remote", | |
| "Country": "Cambodia", | |
| "Current Designation": "Senior Software Engineer", | |
| "Current Company": "no", | |
| "Expected CTC": "500", | |
| "Notice Period": "Immediate", | |
| "Total Work Experience": "15", | |
| "How Many Years of working in a role utilizing your Networking Experience, do you have?": "Master of Science in Computer Science", | |
| "Networking Experience": "7+ Years", | |
| "What does CDN stand for and what is it?": "Content Delivery Network.", | |
| "Do you require a work permission?": "No", | |
| "Address": "Phnom Penh", | |
| "City": "Phnom Penh", | |
| "State": "Phnom Penh", | |
| "Postal Code": "12000", | |
| "How many years of experience do you have in Customer Service?": "15", | |
| "How many years of experience do you have in Customer Support?": "15", | |
| "Please describe any certifications and experience you have with Salesforce": "I have extensive experience with Salesforce, Apex and Lightning Web Components.", | |
| "Please describe your experience with GIT or other source control systems.": "I have extensive experience with GIT, including branching strategies, pull requests, and source control management.", | |
| "Please describe your experience with DevOps and the Software Development Lifecycle.": "I have worked with CI/CD pipelines, Docker, and the full Software Development Lifecycle.", | |
| "Please briefly describe any experience working at a startup. What do you like most about working at a startup?": "I enjoy the fast-paced, high-impact environment of startups where I can contribute to core product decisions." | |
| }; | |
| // 2. HELPERS: Logic extracted into clean methods | |
| const FieldHelper = { | |
| normalize: (s) => s ? s.replace(/\s+/g, ' ').replace(/\*/g, '').trim().toLowerCase() : "", | |
| getType: (container) => { | |
| if (container.querySelector('input[type="radio"]')) return "radio"; | |
| if (container.querySelector('select')) return "select"; | |
| if (container.querySelector('textarea')) return "textarea"; | |
| if (container.querySelector('input[type="number"]')) return "number"; | |
| return "text"; | |
| }, | |
| // Checks if field is empty or Indeed flagged it red | |
| isValid: (type, value, container) => { | |
| if (!value || value.trim() === "") return false; | |
| if (container.querySelector('[aria-invalid="true"]')) return false; | |
| if (container.innerHTML.includes('Choose an option to continue')) return false; | |
| if (type === "number" && isNaN(value)) return false; | |
| if (type === "radio") { | |
| const opts = Array.from(container.querySelectorAll('label')).map(l => FieldHelper.normalize(l.innerText)); | |
| return opts.some(o => o === FieldHelper.normalize(value) || o.includes(FieldHelper.normalize(value))); | |
| } | |
| return true; | |
| }, | |
| fill: (type, container, value) => { | |
| const cleanVal = FieldHelper.normalize(value); | |
| if (type === "radio") { | |
| container.querySelectorAll('label').forEach(lbl => { | |
| const txt = FieldHelper.normalize(lbl.innerText); | |
| if (txt === cleanVal || txt.includes(cleanVal)) { | |
| const i = lbl.querySelector('input'); | |
| if (i) { i.checked = true; i.click(); i.dispatchEvent(new Event('change', {bubbles:true})); } | |
| lbl.click(); | |
| lbl.querySelectorAll('span').forEach(s => s.click()); | |
| } | |
| }); | |
| } else if (type === "select") { | |
| const s = container.querySelector('select'); | |
| const opt = Array.from(s.options).find(o => FieldHelper.normalize(o.text).includes(cleanVal)); | |
| if (opt) { s.value = opt.value; s.dispatchEvent(new Event('change', {bubbles:true})); } | |
| } else { | |
| const i = container.querySelector('input, textarea'); | |
| if (!i) return; | |
| const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(i), 'value')?.set; | |
| if (setter) setter.call(i, value); | |
| i.dispatchEvent(new Event('input', { bubbles: true })); | |
| i.dispatchEvent(new Event('change', { bubbles: true })); | |
| i.dispatchEvent(new Event('blur', { bubbles: true })); | |
| } | |
| } | |
| }; | |
| // 3. AUTOMATION LOGIC | |
| const Automation = { | |
| getAutoContinue: () => JSON.parse(localStorage.getItem(SETTINGS_KEY))?.autoContinue || false, | |
| // Find "Continue" or "Next" buttons on any page | |
| getContinueButton: () => { | |
| return document.querySelector('button[data-testid*="continue"]') || | |
| document.querySelector('button.mosaic-provider-module-apply-questions-6ja1uy') || | |
| document.querySelector('button.mosaic-provider-module-apply-contact-info-evwlos') || | |
| document.querySelector('.mosaic-provider-module-apply-resume-selection button'); | |
| }, | |
| run: () => { | |
| const isQuestionsPage = !!document.querySelector('.ia-Questions-item'); | |
| let filledCount = 0; | |
| if (isQuestionsPage) { | |
| const questions = document.querySelectorAll('.ia-Questions-item'); | |
| questions.forEach(q => { | |
| const label = FieldHelper.normalize(q.querySelector('label')?.innerText || ""); | |
| const key = Object.keys(FORM_DEFAULTS).find(k => label.includes(FieldHelper.normalize(k))); | |
| if (key) { | |
| FieldHelper.fill(FieldHelper.getType(q), q, FORM_DEFAULTS[key]); | |
| filledCount++; | |
| } | |
| }); | |
| } | |
| // AUTO-CONTINUE logic | |
| if (Automation.getAutoContinue()) { | |
| const btn = Automation.getContinueButton(); | |
| if (!btn) return; | |
| // On questions page, only click if no errors visible | |
| if (isQuestionsPage) { | |
| const hasErrors = !!document.querySelector('[aria-invalid="true"]') || document.body.innerHTML.includes('Choose an option to continue'); | |
| if (!hasErrors) setTimeout(() => btn.click(), 1000); | |
| } else { | |
| // On simple pages (Resume, Profile, Location), just click | |
| setTimeout(() => btn.click(), 1000); | |
| } | |
| } | |
| } | |
| }; | |
| // 4. UI LOGIC (No jitter re-render) | |
| let lastHash = ""; | |
| const render = () => { | |
| if (document.activeElement?.classList.contains('ib-val')) return; | |
| const widget = document.getElementById(WIDGET_ID) || createWidget(); | |
| const body = widget.querySelector('.ib-body'); | |
| const pageQuestions = document.querySelectorAll('.ia-Questions-item'); | |
| const activeMap = new Map(); | |
| let currentHash = ""; | |
| pageQuestions.forEach(q => { | |
| const txt = FieldHelper.normalize(q.querySelector('label')?.innerText || ""); | |
| activeMap.set(txt, { type: FieldHelper.getType(q), container: q }); | |
| currentHash += txt; | |
| }); | |
| // Detect Profile/Resume simple pages | |
| const isSimplePage = !pageQuestions.length && !!Automation.getContinueButton(); | |
| if (isSimplePage) currentHash = "simple-page"; | |
| if (currentHash === lastHash) { | |
| updateValidation(activeMap); | |
| return; | |
| } | |
| lastHash = currentHash; | |
| const keys = Object.keys(FORM_DEFAULTS); | |
| const activeKeys = keys.filter(k => Array.from(activeMap.keys()).some(am => am.includes(FieldHelper.normalize(k)))); | |
| const inactiveKeys = keys.filter(k => !activeKeys.includes(k)); | |
| body.innerHTML = [...activeKeys, ...inactiveKeys].map(k => { | |
| const normK = FieldHelper.normalize(k); | |
| const activeData = Array.from(activeMap.entries()).find(([m]) => m.includes(normK))?.[1]; | |
| const type = activeData ? activeData.type : ""; | |
| const invalid = activeData && !FieldHelper.isValid(type, FORM_DEFAULTS[k], activeData.container); | |
| return ` | |
| <div class="ib-row ${activeData ? 'active' : ''} ${invalid ? 'invalid' : ''}" data-key="${k}"> | |
| <div class="ib-label-row"> | |
| <span class="ib-label">${k}</span> | |
| ${activeData ? `<span class="ib-type">${type.toUpperCase()}</span>` : ''} | |
| </div> | |
| <input type="text" class="ib-val" value="${FORM_DEFAULTS[k]}" readonly /> | |
| </div> | |
| `; | |
| }).join('') + `<button id="ib-manual-fill" class="ib-btn">⚡ FORCE FILL PAGE</button>`; | |
| body.querySelector('#ib-manual-fill').onclick = Automation.run; | |
| }; | |
| const updateValidation = (activeMap) => { | |
| document.querySelectorAll('.ib-row').forEach(row => { | |
| const k = row.dataset.key; | |
| const normK = FieldHelper.normalize(k); | |
| const activeData = Array.from(activeMap.entries()).find(([m]) => m.includes(normK))?.[1]; | |
| if (activeData) { | |
| const valid = FieldHelper.isValid(activeData.type, FORM_DEFAULTS[k], activeData.container); | |
| row.classList.toggle('invalid', !valid); | |
| } | |
| }); | |
| }; | |
| const createWidget = () => { | |
| const widget = document.createElement("div"); | |
| widget.id = WIDGET_ID; | |
| widget.innerHTML = `<div class="ib-head">INDEED AUTO-APPLY v4</div><div class="ib-body"></div><div class="ib-foot"></div>`; | |
| document.body.appendChild(widget); | |
| const foot = widget.querySelector('.ib-foot'); | |
| foot.innerHTML = `<input type="checkbox" id="ib-auto-cb" ${Automation.getAutoContinue() ? 'checked' : ''}><label for="ib-auto-cb">Auto-Continue (All Pages)</label>`; | |
| foot.querySelector('#ib-auto-cb').onchange = (e) => { | |
| localStorage.setItem(SETTINGS_KEY, JSON.stringify({ autoContinue: e.target.checked })); | |
| }; | |
| const head = widget.querySelector('.ib-head'); | |
| let isMoving = false, ox, oy; | |
| head.onmousedown = (e) => { isMoving = true; ox = e.clientX - widget.offsetLeft; oy = e.clientY - widget.offsetTop; }; | |
| document.onmousemove = (e) => { if (isMoving) { widget.style.left = (e.clientX - ox) + 'px'; widget.style.top = (e.clientY - oy) + 'px'; widget.style.right = 'auto'; } }; | |
| document.onmouseup = () => { if (isMoving) { isMoving = false; localStorage.setItem(POS_KEY, JSON.stringify({ left: widget.style.left, top: widget.style.top })); } }; | |
| const saved = JSON.parse(localStorage.getItem(POS_KEY)); | |
| if (saved) Object.assign(widget.style, saved); | |
| return widget; | |
| }; | |
| const injectStyles = () => { | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| #${WIDGET_ID} { position: fixed; width: 330px; background: #fff; border: 2px solid #2557a7; border-radius: 8px; box-shadow: 0 10px 40px #0004; z-index: 10001; font-family: sans-serif; top: 10px; right: 10px; display: flex; flex-direction: column; } | |
| .ib-head { background: #2557a7; color: #fff; padding: 10px; cursor: move; font-size: 11px; font-weight: bold; text-align: center; border-radius: 6px 6px 0 0; } | |
| .ib-body { padding: 8px; max-height: 450px; overflow-y: auto; background: #f9f9f9; } | |
| .ib-row { margin-bottom: 5px; padding: 6px; border-radius: 4px; border: 1px solid #ddd; background: #fff; opacity: 0.7; } | |
| .ib-row.active { opacity: 1; border-color: #38b2ac; background: #e6fffa; border-left: 4px solid #38b2ac; } | |
| .ib-row.invalid { border-color: #e53e3e !important; background: #fff5f5 !important; } | |
| .ib-label-row { display: flex; justify-content: space-between; gap: 4px; margin-bottom: 3px;} | |
| .ib-label { font-size: 9px; font-weight: bold; color: #555; line-height: 1; flex: 1; } | |
| .ib-type { font-size: 8px; font-weight: bold; background: #edf2f7; padding: 1px 3px; border-radius: 2px; } | |
| .ib-val { width: 100%; border: none; background: transparent; font-size: 11px; color: #333; outline: none; pointer-events: none; } | |
| .ib-btn { width: 100%; padding: 10px; background: #2557a7; color: #fff; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; margin-top: 5px; font-size: 11px; } | |
| .ib-foot { font-size: 11px; padding: 8px; border-top: 1px solid #eee; display: flex; align-items: center; gap: 5px; background: #fff; border-radius: 0 0 8px 8px; } | |
| `; | |
| document.head.appendChild(style); | |
| }; | |
| // INIT | |
| injectStyles(); | |
| setInterval(() => { | |
| render(); | |
| Automation.run(); | |
| }, 1500); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Indeed.com: Highlight non-sponsored jobs | |
| // @namespace localhost | |
| // @description This script just highlights sponsored and non-sponsored jobs by different colors for better visualization. | |
| // @include *.indeed.com/* | |
| // @include *.indeed.co.uk/* | |
| // @include *.indeed.*/* | |
| // @run-at document-end | |
| // @author lukie80 | |
| // @copyright Creative Commons Attribution-ShareAlike 3.0 Unported (CC-BY-SA 3.0) | |
| // @license http://creativecommons.org/licenses/by-sa/3.0/ | |
| // @version 1.5 | |
| // @lastupdated 2016.06.15 | |
| // | |
| // @downloadURL https://update.greasyfork.org/scripts/20617/Indeedcom%3A%20Highlight%20non-sponsored%20jobs.user.js | |
| // @updateURL https://update.greasyfork.org/scripts/20617/Indeedcom%3A%20Highlight%20non-sponsored%20jobs.meta.js | |
| // ==/UserScript== | |
| //------------------------------------------------------------------------------------------------------------------- | |
| //source: http://stackoverflow.com/a/9496574 - not needed for script, just here for educational purposes | |
| function getAllElementsWithAttribute(attribute) | |
| { | |
| var matchingElements = []; | |
| var allElements = document.getElementsByTagName('*'); | |
| for (var i = 0, n = allElements.length; i < n; i++) | |
| { | |
| if (allElements[i].getAttribute(attribute) !== null) | |
| { | |
| // Element exists with attribute. Add to array. | |
| matchingElements.push(allElements[i]); | |
| } | |
| } | |
| return matchingElements; | |
| } | |
| //source: http://stackoverflow.com/a/4275177 - needed | |
| function getElementsStartsWithId( id ) { | |
| var children = document.body.getElementsByTagName('*'); | |
| var elements = [], child; | |
| for (var i = 0, length = children.length; i < length; i++) { | |
| child = children[i]; | |
| if (child.id.substr(0, id.length) == id) | |
| elements.push(child); | |
| } | |
| return elements; | |
| } | |
| var goodDivs = getElementsStartsWithId("p_"); | |
| for (var i = 0; i < goodDivs.length; i++){ | |
| goodDivs[i].style.background = '#F8F8F8'; | |
| } | |
| if (getElementsStartsWithId("pj_")[0]){ | |
| var badDivs = getElementsStartsWithId("pj_"); | |
| for (var i = 0; i < badDivs.length; i++){ | |
| badDivs[i].style.background = '#fdf9fd'; | |
| badDivs[i].style.border = 'thin solid #f7e6f7'; | |
| badDivs[i].style.margin = "-1px -1px -1px -1px"; | |
| //badDivs[i].remove(); | |
| //this can remove the sponsored jobs but this is not suggested | |
| //because they are not spam. However they are | |
| //quantitative spam. | |
| } | |
| } | |
| // create link company job search | |
| if (document.getElementsByClassName("company")){ | |
| var companyEles = document.getElementsByClassName("company"); | |
| for (var i = 0; i < companyEles.length; i++){ | |
| var companyLinkEle = document.createElement('a'); | |
| companyLinkEle.href = "http:\/\/"+window.location.href.match(/[\w.]*indeed[\w.]+/)+"\/jobs?q=company:\""+encodeURI(companyEles[i].textContent.replace(/^\s*/, ''))+"\"&l="; | |
| companyLinkEle.innerHTML = companyEles[i].textContent; | |
| companyLinkEle.setAttribute("target", "_blank"); | |
| companyEles[i].textContent=""; | |
| companyEles[i].appendChild(companyLinkEle); | |
| } | |
| } | |
| //------------------------------------------------------------------------------------------------------------------- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name WorkUa Quick Apply Pro | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.3.0 | |
| // @description Automate job applications on Work.ua directly via Tampermonkey | |
| // @author You | |
| // @match https://www.work.ua/* | |
| // @grant none | |
| // ==/UserScript== | |
| (() => { | |
| "use strict"; | |
| /* ======================================================================== | |
| 1. CONFIG & CONSTANTS | |
| ======================================================================== */ | |
| const KEYS = { STATE: "workua_bot_state", POS: "workua_bot_pos" }; | |
| const DEFAULT_STATE = { | |
| queue: [], | |
| coverLetter: "Really want to be part of Your team. Ok from Asia? https://srghma.github.io/resume/", | |
| resumeKeyword: "Full stack developer", | |
| isRunning: false, | |
| status: "Idle", | |
| }; | |
| const CLASSES = { | |
| legacyWrapper: 'custom-job-checkbox-group btn btn-tertiary btn-icon btn-icon-left mr-lg', | |
| tailwindWrapper: 'tw-font-medium tw-py-[7px] tw-min-h-[40px] tw-rounded-lg tw-whitespace-nowrap md:tw-text-var-md md:tw-font-semibold tw-px-sm tw-text-gray-900 tw-inline-flex tw-items-center tw-justify-center tw-mr-lg', | |
| }; | |
| /* ======================================================================== | |
| 2. UTILS | |
| ======================================================================== */ | |
| const Utils = { | |
| sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), | |
| isVisible: (el) => { | |
| if (!el || !(el instanceof HTMLElement)) return false; | |
| const style = window.getComputedStyle(el); | |
| if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false; | |
| return el.getBoundingClientRect().width > 0; | |
| }, | |
| waitForElement: async (selector, timeout = 5000) => { | |
| const start = Date.now(); | |
| while (Date.now() - start < timeout) { | |
| const el = document.querySelector(selector); | |
| if (Utils.isVisible(el)) return el; | |
| await Utils.sleep(200); | |
| } | |
| return null; | |
| }, | |
| findByText: (selector, text) => | |
| Array.from(document.querySelectorAll(selector)).find(el => el.textContent.includes(text)), | |
| // React relies on internal state; we must bypass it to set input values natively | |
| setReactInput: (element, value) => { | |
| const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; | |
| setter.call(element, value); | |
| element.dispatchEvent(new Event("input", { bubbles: true })); | |
| element.dispatchEvent(new Event("change", { bubbles: true })); | |
| }, | |
| // Blocks events from bubbling up to parent listeners (Fixes the redirect bug) | |
| stopEventBubbling: (el) => { | |
| ['click', 'mousedown', 'mouseup', 'pointerdown'].forEach(evt => | |
| el.addEventListener(evt, e => e.stopPropagation()) | |
| ); | |
| } | |
| }; | |
| /* ======================================================================== | |
| 3. STATE STORE | |
| ======================================================================== */ | |
| const Store = { | |
| get: () => { | |
| try { return JSON.parse(localStorage.getItem(KEYS.STATE)) || DEFAULT_STATE; } | |
| catch { return DEFAULT_STATE; } | |
| }, | |
| set: (state) => { | |
| localStorage.setItem(KEYS.STATE, JSON.stringify(state)); | |
| UI.render(); // Always sync UI when state changes | |
| }, | |
| update: (updates) => Store.set({ ...Store.get(), ...updates }) | |
| }; | |
| /* ======================================================================== | |
| 4. BOT LOGIC (MACRO) | |
| ======================================================================== */ | |
| const Bot = { | |
| finishJob: (reason, isSuccess = true) => { | |
| console.log(`[WorkUa Bot] ${reason}`); | |
| const state = Store.get(); | |
| Store.update({ | |
| queue: state.queue.slice(1), | |
| status: isSuccess ? "Job applied! Next..." : `Skipped: ${reason}` | |
| }); | |
| setTimeout(Bot.processQueue, 1500); | |
| }, | |
| pause: (reason) => { | |
| console.warn(`[WorkUa Bot] Paused: ${reason}`); | |
| Store.update({ isRunning: false, status: reason }); | |
| }, | |
| processQueue: () => { | |
| const state = Store.get(); | |
| if (!state.isRunning) return; | |
| if (state.queue.length === 0) return Store.update({ isRunning: false, status: "Queue finished." }); | |
| const currentJob = state.queue[0]; | |
| if (!currentJob.active) return Bot.finishJob("Job unchecked, skipping...", false); | |
| const currentIdOnPage = window.location.href.match(/\/jobs\/(\d+)/)?.[1]; | |
| if (currentIdOnPage === currentJob.id) { | |
| setTimeout(Bot.runMacro, 1000); // Execute Macro | |
| } else { | |
| Store.update({ status: `Loading ${currentJob.title.substring(0, 15)}...` }); | |
| const urlObj = new URL(currentJob.url, window.location.origin); | |
| urlObj.searchParams.set('modal', 'send-resume'); | |
| window.location.href = urlObj.toString(); // Navigate | |
| } | |
| }, | |
| runMacro: async () => { | |
| console.log("[WorkUa Bot] Starting macro..."); | |
| Store.update({ status: "Running automation..." }); | |
| const state = Store.get(); | |
| await Utils.sleep(1500); | |
| // 1. Check if already applied | |
| if (Utils.isVisible(document.querySelector('.already-sent'))) { | |
| return Bot.finishJob("Already applied to this job", false); | |
| } | |
| // 2. Locate or trigger the modal form | |
| let formEl = await Utils.waitForElement('form[autocomplete="off"].tw-card', 5000); | |
| const noApplyBtn = Utils.findByText('button', 'Ні, не відгукатися'); | |
| if (noApplyBtn && Utils.isVisible(noApplyBtn)) { | |
| noApplyBtn.click(); | |
| await Utils.sleep(500); | |
| return Bot.finishJob("Skipped: Apply again modal detected", false); | |
| } | |
| if (!formEl) { | |
| const applyBtn = document.querySelector('button[data-open-react-send-resume-modal="true"]'); | |
| if (applyBtn && Utils.isVisible(applyBtn)) { | |
| applyBtn.click(); | |
| formEl = await Utils.waitForElement('form[autocomplete="off"].tw-card', 5000); | |
| } | |
| if (!formEl) return Bot.pause("Modal form did not load in time"); | |
| } | |
| // 3. Select Resume | |
| const labels = Array.from(document.querySelectorAll('form.tw-card label')); | |
| const targetLabel = labels.find(l => l.innerText.toLowerCase().includes(state.resumeKeyword.toLowerCase())); | |
| if (targetLabel) { | |
| const radio = document.getElementById(targetLabel.getAttribute('for')); | |
| if (radio && !radio.disabled) radio.click(); | |
| } else { | |
| const fallbackRadio = document.querySelector('input[name="resume"]:not([disabled])'); | |
| if (fallbackRadio) fallbackRadio.click(); | |
| else return Bot.pause("No valid resume found to select."); | |
| } | |
| await Utils.sleep(500); | |
| // 4. Fill Cover Letter | |
| const addDescCheckbox = document.querySelector('input[name="addDescription"]'); | |
| if (addDescCheckbox && !addDescCheckbox.checked) { | |
| addDescCheckbox.click(); | |
| await Utils.sleep(500); | |
| } | |
| const textarea = await Utils.waitForElement('textarea[name="description"]', 3000); | |
| if (textarea) Utils.setReactInput(textarea, state.coverLetter); | |
| else return Bot.pause("Description textarea not found"); | |
| await Utils.sleep(1000); | |
| // 5. Submit | |
| const submitBtn = Utils.findByText('button[type="submit"]', 'Надіслати'); | |
| if (submitBtn && Utils.isVisible(submitBtn)) { | |
| submitBtn.click(); | |
| } else { | |
| return Bot.pause("Submit button not found"); | |
| } | |
| await Utils.sleep(3000); | |
| Bot.finishJob("Successfully Applied", true); | |
| } | |
| }; | |
| /* ======================================================================== | |
| 5. UI MODIFIERS (Widget & List Injector) | |
| ======================================================================== */ | |
| const UI = { | |
| init: () => { | |
| UI.injectGlobalStyles(); | |
| UI.createWidget(); | |
| const observer = new MutationObserver(() => UI.injectCheckboxesToList()); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| UI.injectCheckboxesToList(); | |
| }, | |
| injectGlobalStyles: () => { | |
| if (document.getElementById('wk-bot-styles')) return; | |
| document.head.insertAdjacentHTML('beforeend', ` | |
| <style id="wk-bot-styles"> | |
| #wk-bot-widget { position: fixed; width: 340px; background: white; border: 2px solid #0056b3; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); z-index: 999999; font-family: ui-sans-serif, system-ui, sans-serif; color: #1f2937; } | |
| .wk-drag-handle { background: #e6f0fa; color: #0056b3; font-size: 10px; font-weight: bold; text-align: center; padding: 6px; cursor: move; user-select: none; border-radius: 6px 6px 0 0; border-bottom: 1px solid #b3d4ff; } | |
| .wk-content { padding: 12px; } | |
| .wk-input-group { margin-bottom: 8px; } | |
| .wk-input-group label { display: block; font-size: 11px; font-weight: bold; margin-bottom: 2px; } | |
| .wk-input-group input, .wk-input-group textarea { width: 100%; padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box; font-size:11px;} | |
| .wk-input-group textarea { resize: vertical; height: 50px; } | |
| .wk-queue-container { max-height: 150px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 4px; padding: 6px; margin-bottom: 12px; } | |
| .wk-queue-header { font-size: 10px; font-weight: bold; text-transform: uppercase; color: #6b7280; margin-bottom: 6px; } | |
| .wk-queue-item { display: flex; align-items: center; gap: 8px; padding: 6px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 4px; background: white; } | |
| .wk-queue-item.wk-active { background: #eff6ff; border-color: #93c5fd; } | |
| .wk-queue-item a { font-size: 10px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #0056b3; text-decoration: none; } | |
| .wk-queue-item button { background: none; border: none; color: #ef4444; cursor: pointer; padding: 2px; font-size: 12px; } | |
| .wk-actions { display: flex; gap: 8px; } | |
| .wk-btn { flex: 1; padding: 8px; border: none; border-radius: 4px; font-weight: bold; font-size: 12px; cursor: pointer; } | |
| .wk-btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .wk-btn-green { background: #16a34a; color: white; } | |
| .wk-btn-yellow { background: #eab308; color: white; } | |
| .wk-btn-gray { background: #e5e7eb; color: #4b5563; flex: 0.3; } | |
| .wk-btn-blue { background: #0056b3; color: white; width: 100%; margin-top: 8px; } | |
| .wk-status { margin-top: 10px; font-size: 10px; text-align: center; font-weight: 500; color: #6b7280; } | |
| </style> | |
| `); | |
| }, | |
| makeDraggable: (handle, widget) => { | |
| let isDragging = false, startX, startY, initX, initY; | |
| handle.addEventListener("mousedown", (e) => { | |
| isDragging = true; startX = e.clientX; startY = e.clientY; | |
| initX = widget.offsetLeft; initY = widget.offsetTop; | |
| document.body.style.userSelect = "none"; | |
| }); | |
| document.addEventListener("mousemove", (e) => { | |
| if (!isDragging) return; | |
| widget.style.left = `${initX + (e.clientX - startX)}px`; | |
| widget.style.top = `${initY + (e.clientY - startY)}px`; | |
| }); | |
| document.addEventListener("mouseup", () => { | |
| if (!isDragging) return; | |
| isDragging = false; | |
| document.body.style.userSelect = ""; | |
| localStorage.setItem(KEYS.POS, JSON.stringify({ left: widget.style.left, top: widget.style.top })); | |
| }); | |
| }, | |
| createWidget: () => { | |
| if (document.getElementById('wk-bot-widget')) return; | |
| const isJobPage = /^\/jobs\/\d+/.test(window.location.pathname); | |
| const pos = JSON.parse(localStorage.getItem(KEYS.POS) || '{"left":"20px","top":"100px"}'); | |
| document.body.insertAdjacentHTML('beforeend', ` | |
| <div id="wk-bot-widget" style="left:${pos.left}; top:${pos.top};"> | |
| <div class="wk-drag-handle" id="wk-drag-handle">≡ DRAG TO REPOSITION ≡</div> | |
| <div class="wk-content"> | |
| <div class="wk-input-group"><label>Resume Keyword</label><input type="text" id="wk-resume" /></div> | |
| <div class="wk-input-group"><label>Cover Letter Text</label><textarea id="wk-cover"></textarea></div> | |
| <div class="wk-queue-container"> | |
| <div class="wk-queue-header">Queue (<span id="wk-q-count">0</span>)</div> | |
| <div id="wk-queue-list"></div> | |
| </div> | |
| <div class="wk-actions"> | |
| <button id="wk-btn-run" class="wk-btn wk-btn-green">▶ RUN</button> | |
| <button id="wk-btn-pause" class="wk-btn wk-btn-yellow" style="display:none">⏸ PAUSE</button> | |
| <button id="wk-btn-clear" class="wk-btn wk-btn-gray">⏹</button> | |
| </div> | |
| ${isJobPage ? `<button id="wk-btn-apply-current" class="wk-btn wk-btn-blue">⚡ Apply for Current Job</button>` : ""} | |
| <div class="wk-status">Status: <span id="wk-status-text">Idle</span></div> | |
| </div> | |
| </div> | |
| `); | |
| UI.makeDraggable(document.getElementById("wk-drag-handle"), document.getElementById("wk-bot-widget")); | |
| UI.attachWidgetListeners(); | |
| UI.render(); | |
| }, | |
| attachWidgetListeners: () => { | |
| const el = id => document.getElementById(id); | |
| el("wk-resume").addEventListener("input", e => Store.update({ resumeKeyword: e.target.value })); | |
| el("wk-cover").addEventListener("input", e => Store.update({ coverLetter: e.target.value })); | |
| el("wk-btn-clear").addEventListener("click", () => Store.update({ isRunning: false, queue: [], status: "Queue cleared" })); | |
| el("wk-btn-pause").addEventListener("click", () => Store.update({ isRunning: false, status: "Paused manually" })); | |
| el("wk-btn-run").addEventListener("click", () => { | |
| Store.update({ isRunning: true }); | |
| Bot.processQueue(); | |
| }); | |
| // Handle individual queue items | |
| document.getElementById("wk-queue-list").addEventListener("change", (e) => { | |
| if(e.target.classList.contains("wk-q-cb")) { | |
| const queue = Store.get().queue.map(q => q.id === e.target.dataset.id ? { ...q, active: e.target.checked } : q); | |
| Store.update({ queue }); | |
| } | |
| }); | |
| document.getElementById("wk-queue-list").addEventListener("click", (e) => { | |
| if(e.target.classList.contains("wk-q-del")) { | |
| const queue = Store.get().queue.filter(q => q.id !== e.target.dataset.id); | |
| Store.update({ queue }); | |
| } | |
| }); | |
| const applyCurrentBtn = el("wk-btn-apply-current"); | |
| if (applyCurrentBtn) { | |
| applyCurrentBtn.addEventListener("click", () => { | |
| const jobId = window.location.href.match(/\/jobs\/(\d+)/)?.[1]; | |
| if (!jobId) return; | |
| const url = window.location.href.split('?')[0]; | |
| const title = document.querySelector("h1")?.innerText || "Current Job"; | |
| const queue = [{ id: jobId, url, title, active: true }, ...Store.get().queue.filter(q => q.id !== jobId)]; | |
| Store.update({ queue, isRunning: true }); | |
| Bot.processQueue(); | |
| }); | |
| } | |
| }, | |
| render: () => { | |
| const state = Store.get(); | |
| const el = id => document.getElementById(id); | |
| // Update Inputs safely | |
| if (document.activeElement !== el("wk-resume")) { | |
| el("wk-resume").value = state.resumeKeyword; | |
| el("wk-resume").disabled = state.isRunning; | |
| } | |
| if (document.activeElement !== el("wk-cover")) { | |
| el("wk-cover").value = state.coverLetter; | |
| el("wk-cover").disabled = state.isRunning; | |
| } | |
| el("wk-q-count").innerText = state.queue.length; | |
| el("wk-status-text").innerText = state.status; | |
| // Update Queue HTML | |
| el("wk-queue-list").innerHTML = state.queue.length === 0 | |
| ? `<div style="font-size:10px; color:#9ca3af;">Select jobs from the search list...</div>` | |
| : state.queue.map((item, i) => ` | |
| <div class="wk-queue-item ${i === 0 ? "wk-active" : ""}"> | |
| <input type="checkbox" class="wk-q-cb" data-id="${item.id}" ${item.active ? "checked" : ""}> | |
| <a href="${item.url}" target="_blank" title="${item.title}">${item.title}</a> | |
| <button class="wk-q-del" data-id="${item.id}">✖</button> | |
| </div> | |
| `).join(""); | |
| // Update Action Buttons | |
| el("wk-btn-run").style.display = state.isRunning ? "none" : "flex"; | |
| el("wk-btn-run").disabled = state.queue.length === 0; | |
| el("wk-btn-pause").style.display = state.isRunning ? "flex" : "none"; | |
| // Sync injected page checkboxes | |
| document.querySelectorAll(".helper-cb").forEach(cb => { | |
| cb.checked = state.queue.some(q => q.id === cb.dataset.jobid); | |
| }); | |
| }, | |
| parseCardForInjection: (card) => { | |
| if (card.classList.contains('helper-ready') || card.innerHTML.includes('Вже відгукнулися')) return null; | |
| const isNew = card.classList.contains('tw-card'); | |
| const titleEl = card.querySelector('h2 a'); | |
| if (!titleEl) return null; | |
| const url = titleEl.href.split('?')[0]; | |
| const title = titleEl.innerText.trim(); | |
| const jobId = isNew ? card.id.replace('job-', '') : (card.getAttribute('data-id') || url.match(/\/jobs\/(\d+)/)?.[1]); | |
| const refElement = isNew | |
| ? Array.from(card.querySelectorAll('button')).find(b => b.innerHTML.includes('icon-visibility-off') || b.innerText.includes('Не показувати')) | |
| : card.querySelector('button.js-hide-job'); | |
| if (!refElement || !refElement.parentElement) return null; | |
| return { card, isNew, url, title, jobId, actionContainer: refElement.parentElement, refElement }; | |
| }, | |
| injectCheckboxesToList: () => { | |
| const rawCards = Array.from(document.querySelectorAll('.card.job-link, .tw-card[id^="job-"]')); | |
| const validCards = rawCards.map(UI.parseCardForInjection).filter(Boolean); | |
| validCards.forEach(({ card, isNew, url, title, jobId, actionContainer, refElement }) => { | |
| card.classList.add('helper-ready'); | |
| const wrapper = document.createElement('label'); | |
| wrapper.className = isNew ? CLASSES.tailwindWrapper : CLASSES.legacyWrapper; | |
| Object.assign(wrapper.style, isNew | |
| ? { cursor: 'pointer', marginBottom: '0' } | |
| : { display: 'inline-flex', alignItems: 'center', cursor: 'pointer', padding: '7px 10px', borderRadius: '10px', fontWeight: 'normal', marginBottom: '0', fontSize: '18px' } | |
| ); | |
| // PREVENT REDIRECT BUG: Blocks all parent click listeners from hijacking the event | |
| Utils.stopEventBubbling(wrapper); | |
| const cb = document.createElement('input'); | |
| cb.type = 'checkbox'; | |
| cb.className = 'helper-cb'; | |
| cb.dataset.jobid = jobId; | |
| cb.checked = Store.get().queue.some(q => q.id === jobId); | |
| cb.style.cssText = `appearance: checkbox !important; width: 20px !important; height: 20px !important; margin: 0 12px 0 0 !important; cursor: pointer !important; position: relative !important; z-index: 1000 !important; accent-color: #007bff !important;`; | |
| const text = document.createElement('span'); | |
| text.innerText = 'Fast submit'; | |
| text.className = isNew ? 'tw-hidden md:tw-inline-block' : 'hidden-xs'; | |
| wrapper.append(cb, text); | |
| // Sync with local storage safely | |
| cb.addEventListener('change', (e) => { | |
| const state = Store.get(); | |
| const exists = state.queue.some(q => q.id === jobId); | |
| if (e.target.checked && !exists) { | |
| Store.update({ queue: [...state.queue, { id: jobId, url, title, active: true }] }); | |
| } else if (!e.target.checked && exists) { | |
| Store.update({ queue: state.queue.filter(q => q.id !== jobId) }); | |
| } | |
| }); | |
| actionContainer.insertBefore(wrapper, refElement); | |
| }); | |
| } | |
| }; | |
| /* ======================================================================== | |
| 6. BOOTSTRAP | |
| ======================================================================== */ | |
| const init = () => { | |
| console.log("[WorkUa Bot] Initializing..."); | |
| UI.init(); | |
| // Auto-resume queue if it was running | |
| const state = Store.get(); | |
| if (state.isRunning && state.queue.length > 0) { | |
| setTimeout(Bot.processQueue, 1500); | |
| } | |
| }; | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment