Auto-scroll job listings, fill applications, and submit your CV/Resume on Indeed, Jobs.cz, Prace.cz and more.
// ==UserScript==
// @license MIT
// @name JobFiller Pro
// @namespace https://github.com/jobfiller-pro
// @version 2.1.0
// @description Auto-scroll job listings, fill applications, and submit your CV/Resume on Indeed, Jobs.cz, Prace.cz and more.
// @author JobFiller Pro
// @match *://*.indeed.com/*
// @match *://*.indeed.co.uk/*
// @match *://*.jobs.cz/*
// @match *://*.prace.cz/*
// @match *://*.linkedin.com/*
// @match *://*.glassdoor.com/*
// @match *://*.pracezarohem.cz/*
// @match *://*.jobscanner.cz/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ═══════════════════════════════════════════════════════════════════════════
// STORAGE (GM_getValue / GM_setValue wrappers)
// ═══════════════════════════════════════════════════════════════════════════
const Store = {
get: (key, def) => GM_getValue(key, def),
set: (key, val) => GM_setValue(key, val),
del: (key) => GM_deleteValue(key),
getProfile: () => GM_getValue('profile', {}),
setProfile: (p) => GM_setValue('profile', p),
getSettings: () => GM_getValue('settings', {
autoScroll: false,
autoFill: true,
autoSubmit: false,
confirmSubmit: true,
highlightFields: true,
scrollDelay: 2500,
fillDelay: 800,
filePriority: 'cv',
}),
setSettings: (s) => GM_setValue('settings', s),
getCVMeta: () => GM_getValue('cvMeta', null),
setCVMeta: (m) => GM_setValue('cvMeta', m),
getCVData: () => GM_getValue('cvData', null),
setCVData: (d) => GM_setValue('cvData', d),
getResumeMeta: () => GM_getValue('resumeMeta', null),
setResumeMeta: (m) => GM_setValue('resumeMeta', m),
getResumeData: () => GM_getValue('resumeData', null),
setResumeData: (d) => GM_setValue('resumeData', d),
getLogs: () => GM_getValue('logs', []),
addLog: (msg, type = 'info') => {
const logs = GM_getValue('logs', []);
logs.push({ msg, type, time: new Date().toTimeString().slice(0, 5) });
if (logs.length > 100) logs.splice(0, logs.length - 100);
GM_setValue('logs', logs);
},
};
// ═══════════════════════════════════════════════════════════════════════════
// FIELD DETECTION ENGINE
// ═══════════════════════════════════════════════════════════════════════════
const FIELD_MAP = {
firstName: {
labels: ['first name', 'given name', 'jméno', 'kerstián'],
attrs: ['firstname', 'first_name', 'fname', 'given_name', 'applicant_first'],
},
lastName: {
labels: ['last name', 'surname', 'příjmení', 'family name'],
attrs: ['lastname', 'last_name', 'lname', 'surname', 'family_name', 'applicant_last'],
},
fullName: {
labels: ['full name', 'celé jméno', 'jméno a příjmení', 'your name', '^name$'],
attrs: ['fullname', 'full_name', 'name', 'applicant_name'],
},
email: {
labels: ['email', 'e-mail', 'emailová adresa', 'email address'],
attrs: ['email', 'email_address', 'e_mail', 'applicant_email'],
inputType: 'email',
},
phone: {
labels: ['phone', 'telephone', 'telefon', 'tel', 'mobil', 'phone number'],
attrs: ['phone', 'tel', 'telephone', 'mobile', 'phone_number', 'applicant_phone'],
inputType: 'tel',
},
street: {
labels: ['street', 'address', 'ulice', 'adresa', 'street address'],
attrs: ['street', 'address', 'address1', 'addr1', 'street_address'],
},
city: {
labels: ['city', 'town', 'město', 'obec'],
attrs: ['city', 'town', 'locality'],
},
zip: {
labels: ['zip', 'postal', 'postcode', 'psč', 'psc', 'zip code'],
attrs: ['zip', 'zipcode', 'postal_code', 'postcode'],
},
country: {
labels: ['country', 'stát', 'země', 'zeme'],
attrs: ['country', 'nation'],
},
linkedin: {
labels: ['linkedin', 'linkedin url', 'linkedin profile'],
attrs: ['linkedin', 'linkedin_url'],
inputType: 'url',
},
coverLetter: {
labels: ['cover letter', 'motivační dopis', 'covering letter', 'motivation', 'message', 'about yourself', 'tell us', 'additional information', 'o sobě'],
attrs: ['cover_letter', 'coverletter', 'motivation', 'message', 'additional_info'],
tag: 'textarea',
},
};
function norm(s) { return (s || '').toLowerCase().replace(/[\s_\-\.]+/g, ' ').trim(); }
function getLabelText(el) {
if (el.id) {
const lbl = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
if (lbl) return lbl.innerText;
}
const wrap = el.closest('label');
if (wrap) return wrap.innerText;
let sib = el.previousElementSibling;
while (sib) {
if (sib.matches('label, [class*="label"], [class*="Label"]')) return sib.innerText;
sib = sib.previousElementSibling;
}
const parent = el.parentElement;
if (parent) {
const lbl = parent.querySelector('label, [class*="label"]');
if (lbl) return lbl.innerText;
}
return el.getAttribute('aria-label') || el.getAttribute('placeholder') || '';
}
function detectIntent(el) {
const tag = el.tagName.toLowerCase();
const type = (el.getAttribute('type') || '').toLowerCase();
const nameId = norm(el.name + ' ' + el.id + ' ' + (el.getAttribute('autocomplete') || ''));
const labelText = norm(getLabelText(el));
const placeholder = norm(el.getAttribute('placeholder') || '');
const combined = nameId + ' ' + labelText + ' ' + placeholder;
for (const [intent, rules] of Object.entries(FIELD_MAP)) {
if (rules.tag && tag !== rules.tag) continue;
if (rules.inputType && type !== rules.inputType && type !== 'text' && type !== '') continue;
if (tag === 'input' && ['hidden','file','submit','button','checkbox','radio','image'].includes(type)) continue;
for (const attr of (rules.attrs || [])) {
if (combined.includes(norm(attr))) return intent;
}
for (const lbl of (rules.labels || [])) {
if (combined.includes(norm(lbl))) return intent;
}
}
return null;
}
function buildValueMap(profile) {
const full = [profile.firstName, profile.lastName].filter(Boolean).join(' ');
return { ...profile, fullName: full };
}
function fillInput(el, value) {
if (!value || el.disabled || el.readOnly) return false;
const tag = el.tagName.toLowerCase();
if (tag === 'select') {
const v = value.toLowerCase();
const opt = Array.from(el.options).find(o =>
o.value.toLowerCase().includes(v) || o.text.toLowerCase().includes(v)
);
if (!opt) return false;
el.value = opt.value;
el.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
// Native setter to bypass React/Vue controlled inputs
const proto = tag === 'textarea' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
if (setter) setter.call(el, value);
else el.value = value;
['input', 'change', 'blur'].forEach(evt =>
el.dispatchEvent(new Event(evt, { bubbles: true }))
);
el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'a' }));
return true;
}
// ═══════════════════════════════════════════════════════════════════════════
// FILE INJECTION
// ═══════════════════════════════════════════════════════════════════════════
function b64toFile(b64, meta) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return new File([bytes], meta.name, { type: meta.type || 'application/pdf' });
}
function injectFile(input, b64, meta) {
try {
const file = b64toFile(b64, meta);
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('input', { bubbles: true }));
return true;
} catch (e) {
return false;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// HIGHLIGHT
// ═══════════════════════════════════════════════════════════════════════════
function highlight(el) {
if (!Store.getSettings().highlightFields) return;
el.style.setProperty('outline', '2px solid #00e5a0', 'important');
el.style.setProperty('outline-offset', '2px', 'important');
setTimeout(() => { el.style.removeProperty('outline'); el.style.removeProperty('outline-offset'); }, 3000);
}
// ═══════════════════════════════════════════════════════════════════════════
// MAIN FILL FUNCTION
// ═══════════════════════════════════════════════════════════════════════════
function fillPage() {
const profile = Store.getProfile();
const settings = Store.getSettings();
if (!profile || !profile.email) return { filled: 0, files: 0, error: 'No profile set up' };
const valueMap = buildValueMap(profile);
let filled = 0, filesInjected = 0;
const done = new Set();
// Text / select / textarea
const els = document.querySelectorAll(
'input:not([type="hidden"]):not([type="file"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]), textarea, select'
);
for (const el of els) {
if (done.has(el)) continue;
if (el.value && el.value.trim().length > 3) continue; // already filled
const intent = detectIntent(el);
if (!intent) continue;
const val = valueMap[intent];
if (!val) continue;
if (fillInput(el, val)) {
highlight(el);
done.add(el);
filled++;
}
}
// File inputs
const cvData = Store.getCVData(), cvMeta = Store.getCVMeta();
const resumeData = Store.getResumeData(), resumeMeta = Store.getResumeMeta();
const fileInputs = Array.from(document.querySelectorAll('input[type="file"]'));
const priority = settings.filePriority || 'cv';
fileInputs.forEach((inp, idx) => {
let data, meta;
if (fileInputs.length === 1) {
data = priority === 'cv' ? (cvData || resumeData) : (resumeData || cvData);
meta = priority === 'cv' ? (cvMeta || resumeMeta) : (resumeMeta || cvMeta);
} else {
data = idx === 0 ? cvData : resumeData;
meta = idx === 0 ? cvMeta : resumeMeta;
}
if (data && meta && injectFile(inp, data, meta)) {
highlight(inp);
filesInjected++;
}
});
const total = filled + filesInjected;
Store.addLog(`Filled ${filled} fields, ${filesInjected} files on ${location.hostname}`, total > 0 ? 'success' : 'info');
return { filled, files: filesInjected };
}
// ═══════════════════════════════════════════════════════════════════════════
// AUTO-SCROLL + AUTO-APPLY ENGINE
// ═══════════════════════════════════════════════════════════════════════════
let scrollTimer = null;
let isScrolling = false;
// Site-specific: find job listing links on the current page
const LISTING_SELECTORS = {
'indeed.com': 'a.jcs-JobTitle, a[data-jk], .jobTitle a, a[id^="job_"]',
'jobs.cz': 'a.SearchResultCard__titleLink, a[data-jobad-id], .job-listing a[href*="/detail/"]',
'prace.cz': 'a.job-title, a[href*="/nabidka/"], .job-card a',
'linkedin.com': 'a.job-card-list__title, a[href*="/jobs/view/"]',
'glassdoor.com': 'a[data-test="job-link"], a[href*="/job-listing/"]',
};
function getListingSelector() {
const host = location.hostname;
for (const [domain, sel] of Object.entries(LISTING_SELECTORS)) {
if (host.includes(domain)) return sel;
}
return 'a[href*="job"], a[href*="position"], a[href*="apply"], a[href*="career"]';
}
function getJobLinks() {
const sel = getListingSelector();
const links = Array.from(document.querySelectorAll(sel));
return links.filter(l => {
const href = l.href || '';
return href && !href.includes('#') && href !== location.href;
});
}
let scrollQueue = [];
let scrollIndex = 0;
let scrollActive = false;
function startAutoScroll() {
if (scrollActive) return;
scrollActive = true;
scrollQueue = getJobLinks();
scrollIndex = 0;
if (scrollQueue.length === 0) {
showToast('⚠️ No job listings found on this page');
scrollActive = false;
return;
}
Store.set('scrollStep', 'detail'); // reset step state for fresh run
showToast(`🔍 Found ${scrollQueue.length} job listings — starting...`);
Store.addLog(`Auto-scroll started: ${scrollQueue.length} listings found`, 'info');
processNextListing();
}
function stopAutoScroll() {
scrollActive = false;
clearTimeout(scrollTimer);
updatePanel();
showToast('⏹ Auto-scroll stopped');
Store.addLog('Auto-scroll stopped by user', 'info');
}
function processNextListing() {
if (!scrollActive || scrollIndex >= scrollQueue.length) {
scrollActive = false;
showToast(`✅ Done! Processed ${scrollIndex} listings`);
Store.addLog(`Auto-scroll complete: ${scrollIndex} listings processed`, 'success');
updatePanel();
return;
}
const link = scrollQueue[scrollIndex];
scrollIndex++;
// Scroll to the link and highlight it
link.scrollIntoView({ behavior: 'smooth', block: 'center' });
link.style.setProperty('outline', '2px solid #7c5cfc', 'important');
setTimeout(() => link.style.removeProperty('outline'), 1500);
updatePanel();
const settings = Store.getSettings();
const delay = parseInt(settings.scrollDelay) || 2500;
// Open the job in the same tab after delay
scrollTimer = setTimeout(() => {
if (!scrollActive) return;
// Store queue state so we can resume on the listing page
Store.set('scrollQueue', scrollQueue.map(l => l.href));
Store.set('scrollIndex', scrollIndex);
Store.set('scrollStep', 'detail'); // fresh start: look for reply button first
Store.set('scrollActive', true);
window.location.href = link.href;
}, delay);
}
// ═══════════════════════════════════════════════════════════════════════════
// REPLY BUTTON DETECTION (Czech + English job sites)
// ═══════════════════════════════════════════════════════════════════════════
// Keywords that mean "apply / reply" — NOT submit-after-filling
const REPLY_KEYWORDS = [
'odpovědět', 'odpovedet', 'reagovat', 'přihlásit se', 'prihlasit se',
'apply now', 'apply for', 'quick apply', 'easy apply',
'send application', 'submit application',
];
// Keywords that mean "send/submit the filled form"
const SUBMIT_KEYWORDS = [
'odeslat', 'potvrdit', 'dokončit', 'dokoncit',
'submit', 'send', 'confirm', 'finish', 'complete',
'přihlásit', 'prihlasit',
];
function findReplyButton() {
// Look for <a> or <button> whose visible text matches reply keywords
const candidates = Array.from(document.querySelectorAll(
'a[href], button, input[type="button"], input[type="submit"], [role="button"]'
));
for (const el of candidates) {
if (el.offsetParent === null) continue; // skip hidden
const txt = (el.textContent || el.value || el.getAttribute('aria-label') || '').trim().toLowerCase();
if (REPLY_KEYWORDS.some(k => txt.includes(k))) return el;
}
return null;
}
function findSubmitButton() {
const candidates = Array.from(document.querySelectorAll(
'button[type="submit"], input[type="submit"], button, [role="button"]'
));
// Prefer exact submit-type buttons first
const submitType = candidates.find(el => {
if (el.offsetParent === null) return false;
const txt = (el.textContent || el.value || '').trim().toLowerCase();
return SUBMIT_KEYWORDS.some(k => txt.includes(k));
});
if (submitType) return submitType;
// Fallback: any submit-type input
return document.querySelector('button[type="submit"], input[type="submit"]') || null;
}
// ═══════════════════════════════════════════════════════════════════════════
// SCROLL SESSION STATE MACHINE
// States: 'listing' → 'detail' → 'form' → back to 'listing'
// ═══════════════════════════════════════════════════════════════════════════
// Called every time a page loads during an active scroll session
function resumeScrollMode() {
const settings = Store.getSettings();
const fillDelay = parseInt(settings.fillDelay) || 800;
const scrollDelay = parseInt(settings.scrollDelay) || 2500;
const step = Store.get('scrollStep', 'detail'); // 'detail' or 'form'
if (step === 'detail') {
// We just landed on the job detail page.
// Look for a reply/apply button and click it automatically.
showToast('🔍 Looking for reply button...');
waitForElement(findReplyButton, 4000).then(btn => {
if (btn) {
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
const label = (btn.textContent || btn.value || '').trim();
showToast(`👆 Clicking "${label}"...`);
Store.addLog(`Clicking reply button: "${label}"`, 'info');
setTimeout(() => {
// If it's a link that navigates to a new page, set step to 'form'
const href = btn.getAttribute('href');
if (href && href !== '#' && !href.startsWith('javascript')) {
Store.set('scrollStep', 'form');
Store.set('scrollActive', true);
btn.click();
} else {
// It might open a modal/inline form on the same page
btn.click();
Store.set('scrollStep', 'form');
// Wait for form to appear then fill it
setTimeout(() => {
fillAndSubmitOrBack(settings, scrollDelay);
}, fillDelay + 500);
}
}, 600);
} else {
// No reply button — maybe this IS the form already, or it's already applied
showToast('⚠️ No reply button found — trying to fill directly');
Store.addLog('No reply button found, attempting direct fill', 'info');
Store.set('scrollStep', 'form');
setTimeout(() => fillAndSubmitOrBack(settings, scrollDelay), fillDelay);
}
});
} else if (step === 'form') {
// We're on the application form page. Fill it.
showToast('⚡ Filling application form...');
setTimeout(() => {
fillAndSubmitOrBack(settings, scrollDelay);
}, fillDelay);
}
}
function fillAndSubmitOrBack(settings, scrollDelay) {
const result = fillPage();
showToast(`✅ Filled ${result.filled} fields, ${result.files} files`);
if (settings.autoSubmit) {
setTimeout(() => {
if (settings.confirmSubmit) {
if (confirm('[JobFiller Pro]\nSubmit this application?\n\nOK = Submit & continue\nCancel = Skip & go back')) {
clickSubmit();
} else {
goBackToListings();
}
} else {
clickSubmit();
}
}, 800);
} else {
// No auto-submit: just go back after a pause so user can review
const pause = Math.max(scrollDelay, 2000);
showToast(`👀 Review & go back in ${Math.round(pause/1000)}s... (or stop scroll)`);
Store.set('scrollStep', 'detail'); // reset for next listing
setTimeout(goBackToListings, pause);
}
}
function clickSubmit() {
const btn = findSubmitButton();
if (btn) {
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
btn.click();
Store.addLog(`Submitted on ${location.hostname}`, 'success');
showToast('📨 Application submitted!');
Store.set('scrollStep', 'detail'); // reset for next listing
setTimeout(goBackToListings, 2500);
}, 400);
} else {
showToast('⚠️ Submit button not found — going back');
Store.addLog('Submit button not found', 'error');
Store.set('scrollStep', 'detail');
setTimeout(goBackToListings, 2000);
}
}
function goBackToListings() {
Store.set('scrollActive', true);
// scrollStep stays as 'detail' for the next job
history.back();
}
// Helper: poll for an element up to `timeout` ms
function waitForElement(finder, timeout = 4000) {
return new Promise(resolve => {
const found = finder();
if (found) { resolve(found); return; }
const start = Date.now();
const interval = setInterval(() => {
const el = finder();
if (el) { clearInterval(interval); resolve(el); return; }
if (Date.now() - start > timeout) { clearInterval(interval); resolve(null); }
}, 250);
});
}
// ═══════════════════════════════════════════════════════════════════════════
// TOAST NOTIFICATION
// ═══════════════════════════════════════════════════════════════════════════
let toastTimer;
function showToast(msg, duration = 3000) {
let el = document.getElementById('jfp-toast');
if (!el) {
el = document.createElement('div');
el.id = 'jfp-toast';
document.body.appendChild(el);
}
el.textContent = msg;
el.style.cssText = `
position:fixed!important;bottom:20px!important;right:20px!important;
background:#0d0d0f!important;color:#00e5a0!important;
font-family:'Courier New',monospace!important;font-size:13px!important;
padding:10px 18px!important;border-radius:8px!important;
border:1px solid #00e5a0!important;
box-shadow:0 4px 24px rgba(0,229,160,0.25)!important;
z-index:2147483647!important;
animation:jfp-in 0.25s ease!important;
max-width:320px!important;word-break:break-word!important;
`;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.remove(), duration);
}
// ═══════════════════════════════════════════════════════════════════════════
// FLOATING PANEL UI
// ═══════════════════════════════════════════════════════════════════════════
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@600;700;800&display=swap');
@keyframes jfp-in { from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)} }
@keyframes jfp-panel-in { from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)} }
@keyframes jfp-pulse { 0%,100%{opacity:1}50%{opacity:0.5} }
#jfp-root * { box-sizing:border-box; margin:0; padding:0; font-family:'Syne',sans-serif; }
#jfp-root ::-webkit-scrollbar { width:3px }
#jfp-root ::-webkit-scrollbar-thumb { background:#2a2a35; border-radius:2px }
#jfp-fab {
position:fixed!important; bottom:24px!important; right:24px!important;
width:52px!important; height:52px!important;
background:linear-gradient(135deg,#00e5a0,#7c5cfc)!important;
border-radius:14px!important; cursor:pointer!important;
display:flex!important; align-items:center!important; justify-content:center!important;
font-size:22px!important; z-index:2147483646!important;
box-shadow:0 4px 20px rgba(0,229,160,0.4)!important;
transition:transform 0.2s,box-shadow 0.2s!important;
user-select:none!important;
}
#jfp-fab:hover { transform:scale(1.08)!important; box-shadow:0 6px 28px rgba(0,229,160,0.5)!important; }
#jfp-panel {
position:fixed!important; bottom:88px!important; right:24px!important;
width:360px!important;
background:#0d0d0f!important;
border:1px solid #2a2a35!important;
border-radius:16px!important;
z-index:2147483645!important;
box-shadow:0 8px 40px rgba(0,0,0,0.6)!important;
animation:jfp-panel-in 0.25s ease!important;
overflow:hidden!important;
max-height:90vh!important;
display:flex!important;
flex-direction:column!important;
}
.jfp-header {
background:linear-gradient(135deg,#0f0f14,#1a1025)!important;
padding:14px 16px!important;
border-bottom:1px solid #2a2a35!important;
display:flex!important; align-items:center!important; gap:10px!important;
}
.jfp-logo { font-size:16px!important; font-weight:800!important; color:#e8e8f0!important; }
.jfp-logo span { color:#00e5a0!important; }
.jfp-close {
margin-left:auto!important; cursor:pointer!important; color:#6b6b80!important;
font-size:18px!important; line-height:1!important; padding:2px 6px!important;
border-radius:4px!important;
}
.jfp-close:hover { color:#e8e8f0!important; background:#1e1e24!important; }
.jfp-tabs {
display:flex!important; background:#0d0d0f!important;
border-bottom:1px solid #2a2a35!important; flex-shrink:0!important;
}
.jfp-tab {
flex:1!important; padding:9px 4px!important; text-align:center!important;
font-size:10px!important; font-weight:700!important; color:#6b6b80!important;
border-bottom:2px solid transparent!important; cursor:pointer!important;
letter-spacing:0.3px!important; transition:all 0.2s!important;
}
.jfp-tab.active { color:#00e5a0!important; border-bottom-color:#00e5a0!important; background:rgba(0,229,160,0.04)!important; }
.jfp-tab:hover:not(.active) { color:#e8e8f0!important; }
.jfp-body { overflow-y:auto!important; flex:1!important; padding:14px 16px!important; }
.jfp-section {
font-size:9px!important; font-weight:700!important; letter-spacing:1.5px!important;
color:#6b6b80!important; text-transform:uppercase!important;
margin:12px 0 8px!important;
}
.jfp-section:first-child { margin-top:0!important; }
.jfp-field { display:flex!important; flex-direction:column!important; gap:3px!important; margin-bottom:7px!important; }
.jfp-field label { font-size:9px!important; font-weight:600!important; color:#6b6b80!important; letter-spacing:0.5px!important; }
.jfp-field input, .jfp-field textarea, .jfp-field select {
background:#16161a!important; border:1px solid #2a2a35!important;
border-radius:6px!important; color:#e8e8f0!important;
font-family:'DM Mono',monospace!important; font-size:11px!important;
padding:7px 9px!important; outline:none!important; width:100%!important;
transition:border-color 0.2s!important;
}
.jfp-field input:focus, .jfp-field textarea:focus { border-color:#00e5a0!important; }
.jfp-field textarea { resize:vertical!important; min-height:55px!important; }
.jfp-row { display:flex!important; gap:7px!important; }
.jfp-row .jfp-field { flex:1!important; }
.jfp-btn {
display:flex!important; align-items:center!important; justify-content:center!important;
gap:5px!important; padding:9px 14px!important; border-radius:7px!important;
border:none!important; cursor:pointer!important; font-family:'Syne',sans-serif!important;
font-weight:700!important; font-size:11px!important; transition:all 0.18s!important;
letter-spacing:0.3px!important; width:100%!important; margin-top:10px!important;
}
.jfp-btn-primary { background:linear-gradient(135deg,#00e5a0,#00c488)!important; color:#000!important; }
.jfp-btn-primary:hover { opacity:0.9!important; transform:translateY(-1px)!important; }
.jfp-btn-scroll { background:linear-gradient(135deg,#7c5cfc,#5a3fd4)!important; color:#fff!important; }
.jfp-btn-scroll:hover { opacity:0.9!important; transform:translateY(-1px)!important; }
.jfp-btn-stop { background:rgba(255,77,106,0.15)!important; color:#ff4d6a!important; border:1px solid rgba(255,77,106,0.3)!important; }
.jfp-btn-stop:hover { background:rgba(255,77,106,0.25)!important; }
.jfp-btn-secondary { background:#1e1e24!important; color:#e8e8f0!important; border:1px solid #2a2a35!important; flex:1!important; }
.jfp-btn-secondary:hover { border-color:#00e5a0!important; color:#00e5a0!important; }
.jfp-btn-row { display:flex!important; gap:7px!important; margin-top:8px!important; }
.jfp-btn-row .jfp-btn { margin-top:0!important; }
.jfp-toggle-row {
display:flex!important; align-items:center!important; justify-content:space-between!important;
padding:8px 10px!important; background:#16161a!important;
border-radius:7px!important; border:1px solid #2a2a35!important; margin-bottom:5px!important;
}
.jfp-toggle-label { font-size:11px!important; font-weight:600!important; color:#e8e8f0!important; }
.jfp-toggle-desc { font-size:9px!important; color:#6b6b80!important; margin-top:1px!important; }
.jfp-toggle { position:relative!important; width:34px!important; height:18px!important; flex-shrink:0!important; }
.jfp-toggle input { opacity:0!important; width:0!important; height:0!important; }
.jfp-slider {
position:absolute!important; inset:0!important;
background:#2a2a35!important; border-radius:18px!important; cursor:pointer!important; transition:background 0.2s!important;
}
.jfp-slider::before {
content:''!important; position:absolute!important;
width:12px!important; height:12px!important;
left:3px!important; top:3px!important;
background:#fff!important; border-radius:50%!important; transition:transform 0.2s!important;
}
.jfp-toggle input:checked + .jfp-slider { background:#00e5a0!important; }
.jfp-toggle input:checked + .jfp-slider::before { transform:translateX(16px)!important; }
.jfp-file-area {
border:1.5px dashed #2a2a35!important; border-radius:7px!important;
padding:10px 12px!important; cursor:pointer!important; transition:all 0.2s!important;
display:flex!important; align-items:center!important; gap:9px!important;
background:#16161a!important; margin-bottom:8px!important;
}
.jfp-file-area:hover { border-color:#00e5a0!important; background:rgba(0,229,160,0.04)!important; }
.jfp-file-area.has-file { border-color:#00e5a0!important; border-style:solid!important; }
.jfp-file-icon { font-size:20px!important; flex-shrink:0!important; }
.jfp-file-name { font-family:'DM Mono',monospace!important; font-size:10px!important; color:#00e5a0!important; }
.jfp-file-prompt { font-size:10px!important; color:#6b6b80!important; }
.jfp-log {
background:#16161a!important; border:1px solid #2a2a35!important;
border-radius:7px!important; padding:8px!important;
font-family:'DM Mono',monospace!important; font-size:10px!important;
max-height:150px!important; overflow-y:auto!important; color:#6b6b80!important;
}
.jfp-log-entry { padding:2px 0!important; border-bottom:1px solid rgba(255,255,255,0.04)!important; display:flex!important; gap:7px!important; }
.jfp-log-time { color:#7c5cfc!important; flex-shrink:0!important; }
.jfp-log-success { color:#00e5a0!important; }
.jfp-log-error { color:#ff4d6a!important; }
.jfp-scroll-status {
background:#16161a!important; border:1px solid #2a2a35!important;
border-radius:7px!important; padding:10px!important; margin-bottom:8px!important;
}
.jfp-scroll-progress {
font-family:'DM Mono',monospace!important; font-size:11px!important;
color:#00e5a0!important; margin-bottom:6px!important;
}
.jfp-progress-bar-bg {
background:#2a2a35!important; border-radius:4px!important; height:4px!important; overflow:hidden!important;
}
.jfp-progress-bar { height:4px!important; background:linear-gradient(90deg,#00e5a0,#7c5cfc)!important; border-radius:4px!important; transition:width 0.4s!important; }
.jfp-divider { height:1px!important; background:#2a2a35!important; margin:10px 0!important; }
.jfp-delay-row { display:flex!important; align-items:center!important; gap:8px!important; margin-bottom:6px!important; }
.jfp-delay-row label { font-size:9px!important; color:#6b6b80!important; font-weight:600!important; white-space:nowrap!important; }
.jfp-delay-row input { flex:1!important; }
`);
let panelVisible = false;
let activeTab = 'apply';
function buildPanel() {
const profile = Store.getProfile();
const settings = Store.getSettings();
const cvMeta = Store.getCVMeta();
const resumeMeta = Store.getResumeMeta();
const logs = Store.getLogs();
const queueLen = Store.get('scrollQueue', []).length;
const queueIdx = Store.get('scrollIndex', 0);
const isActive = Store.get('scrollActive', false) && scrollActive;
const pct = queueLen > 0 ? Math.round((queueIdx / queueLen) * 100) : 0;
return `
<div id="jfp-root">
<div id="jfp-fab" title="JobFiller Pro">⚡</div>
<div id="jfp-panel" style="display:none">
<div class="jfp-header">
<span style="font-size:20px">⚡</span>
<div class="jfp-logo">Job<span>Filler</span> Pro</div>
<div class="jfp-close" id="jfp-close">✕</div>
</div>
<div class="jfp-tabs">
<div class="jfp-tab ${activeTab==='apply'?'active':''}" data-tab="apply">⚡ Apply</div>
<div class="jfp-tab ${activeTab==='scroll'?'active':''}" data-tab="scroll">🔍 Scroll</div>
<div class="jfp-tab ${activeTab==='profile'?'active':''}" data-tab="profile">👤 Profile</div>
<div class="jfp-tab ${activeTab==='files'?'active':''}" data-tab="files">📎 Files</div>
<div class="jfp-tab ${activeTab==='settings'?'active':''}" data-tab="settings">⚙</div>
</div>
<div class="jfp-body">
<!-- APPLY TAB -->
<div class="jfp-pane" id="jfp-pane-apply" style="display:${activeTab==='apply'?'block':'none'}">
<div class="jfp-section">Current Page</div>
<p style="font-size:10px;color:#6b6b80;margin-bottom:10px;line-height:1.5;">
Detects and fills all form fields on this page using your saved profile.
</p>
<button class="jfp-btn jfp-btn-primary" id="jfp-fill-now">⚡ Fill This Page Now</button>
<div class="jfp-btn-row">
<button class="jfp-btn jfp-btn-secondary" id="jfp-submit-btn">📨 Submit Form</button>
<button class="jfp-btn jfp-btn-secondary" id="jfp-clear-btn">🧹 Clear Fields</button>
</div>
<div class="jfp-divider"></div>
<div class="jfp-section">Recent Activity</div>
<div class="jfp-log" id="jfp-mini-log">
${logs.slice(-5).reverse().map(l => `
<div class="jfp-log-entry">
<span class="jfp-log-time">${l.time}</span>
<span class="jfp-log-${l.type}">${l.msg}</span>
</div>`).join('') || '<div style="color:#6b6b80;font-size:10px;padding:4px 0">No activity yet</div>'}
</div>
</div>
<!-- SCROLL TAB -->
<div class="jfp-pane" id="jfp-pane-scroll" style="display:${activeTab==='scroll'?'block':'none'}">
<div class="jfp-section">Auto-Scroll & Apply</div>
<p style="font-size:10px;color:#6b6b80;margin-bottom:10px;line-height:1.5;">
Scans job listings on this page, opens each one, fills the form, and optionally submits. Stay on a job listing/search page.
</p>
<div class="jfp-scroll-status" id="jfp-scroll-status">
<div class="jfp-scroll-progress" id="jfp-scroll-progress">
${isActive ? `Processing ${queueIdx} / ${queueLen}` : 'Ready to scan'}
</div>
<div class="jfp-progress-bar-bg">
<div class="jfp-progress-bar" id="jfp-progress-bar" style="width:${pct}%"></div>
</div>
</div>
${isActive
? `<button class="jfp-btn jfp-btn-stop" id="jfp-stop-scroll">⏹ Stop Auto-Scroll</button>`
: `<button class="jfp-btn jfp-btn-scroll" id="jfp-start-scroll">🔍 Start Auto-Scroll</button>`
}
<div class="jfp-divider"></div>
<div class="jfp-section">Timing</div>
<div class="jfp-delay-row">
<label>Scroll delay (ms)</label>
<input type="number" id="jfp-scroll-delay" value="${settings.scrollDelay || 2500}" min="500" max="10000" step="250" style="background:#16161a;border:1px solid #2a2a35;border-radius:5px;color:#e8e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:5px 8px;width:90px" />
</div>
<div class="jfp-delay-row">
<label>Fill delay (ms)</label>
<input type="number" id="jfp-fill-delay" value="${settings.fillDelay || 800}" min="200" max="5000" step="100" style="background:#16161a;border:1px solid #2a2a35;border-radius:5px;color:#e8e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:5px 8px;width:90px" />
</div>
<div class="jfp-divider"></div>
<div class="jfp-toggle-row">
<div>
<div class="jfp-toggle-label">Auto-submit</div>
<div class="jfp-toggle-desc">Clicks submit button after filling</div>
</div>
<label class="jfp-toggle">
<input type="checkbox" id="jfp-auto-submit" ${settings.autoSubmit ? 'checked' : ''} />
<span class="jfp-slider"></span>
</label>
</div>
<div class="jfp-toggle-row">
<div>
<div class="jfp-toggle-label">Confirm before submit</div>
<div class="jfp-toggle-desc">Show a dialog before each submit</div>
</div>
<label class="jfp-toggle">
<input type="checkbox" id="jfp-confirm-submit" ${settings.confirmSubmit ? 'checked' : ''} />
<span class="jfp-slider"></span>
</label>
</div>
<button class="jfp-btn jfp-btn-primary" id="jfp-save-scroll-settings" style="margin-top:10px">💾 Save Scroll Settings</button>
</div>
<!-- PROFILE TAB -->
<div class="jfp-pane" id="jfp-pane-profile" style="display:${activeTab==='profile'?'block':'none'}">
<div class="jfp-section">Personal Info</div>
<div class="jfp-row">
<div class="jfp-field"><label>First Name</label><input id="jfp-firstName" value="${esc(profile.firstName)}" placeholder="Jan" /></div>
<div class="jfp-field"><label>Last Name</label><input id="jfp-lastName" value="${esc(profile.lastName)}" placeholder="Novák" /></div>
</div>
<div class="jfp-field"><label>Email</label><input id="jfp-email" type="email" value="${esc(profile.email)}" placeholder="[email protected]" /></div>
<div class="jfp-row">
<div class="jfp-field"><label>Phone</label><input id="jfp-phone" value="${esc(profile.phone)}" placeholder="+420 123 456 789" /></div>
<div class="jfp-field"><label>LinkedIn</label><input id="jfp-linkedin" value="${esc(profile.linkedin)}" placeholder="linkedin.com/in/..." /></div>
</div>
<div class="jfp-section">Address</div>
<div class="jfp-field"><label>Street</label><input id="jfp-street" value="${esc(profile.street)}" placeholder="Náměstí Míru 1" /></div>
<div class="jfp-row">
<div class="jfp-field"><label>City</label><input id="jfp-city" value="${esc(profile.city)}" placeholder="Praha" /></div>
<div class="jfp-field"><label>ZIP</label><input id="jfp-zip" value="${esc(profile.zip)}" placeholder="110 00" /></div>
</div>
<div class="jfp-field"><label>Country</label><input id="jfp-country" value="${esc(profile.country)}" placeholder="Czech Republic" /></div>
<div class="jfp-section">Cover Letter</div>
<div class="jfp-field"><textarea id="jfp-coverLetter" placeholder="Your cover letter / motivation text...">${esc(profile.coverLetter)}</textarea></div>
<button class="jfp-btn jfp-btn-primary" id="jfp-save-profile">💾 Save Profile</button>
</div>
<!-- FILES TAB -->
<div class="jfp-pane" id="jfp-pane-files" style="display:${activeTab==='files'?'block':'none'}">
<div class="jfp-section">CV / Životopis</div>
<label for="jfp-cv-input">
<div class="jfp-file-area ${cvMeta ? 'has-file' : ''}" id="jfp-cv-area">
<div class="jfp-file-icon">📄</div>
<div>
<div class="jfp-file-name" id="jfp-cv-name">${cvMeta ? cvMeta.name : 'Click to upload CV'}</div>
<div class="jfp-file-prompt">${cvMeta ? '✓ Saved · ' + formatSize(cvMeta.size) : 'PDF, DOC, DOCX · max 5MB'}</div>
</div>
</div>
</label>
<input type="file" id="jfp-cv-input" accept=".pdf,.doc,.docx" style="display:none" />
<div class="jfp-section">Resume (alternate)</div>
<label for="jfp-resume-input">
<div class="jfp-file-area ${resumeMeta ? 'has-file' : ''}" id="jfp-resume-area">
<div class="jfp-file-icon">📋</div>
<div>
<div class="jfp-file-name" id="jfp-resume-name">${resumeMeta ? resumeMeta.name : 'Click to upload Resume'}</div>
<div class="jfp-file-prompt">${resumeMeta ? '✓ Saved · ' + formatSize(resumeMeta.size) : 'PDF, DOC, DOCX · max 5MB'}</div>
</div>
</div>
</label>
<input type="file" id="jfp-resume-input" accept=".pdf,.doc,.docx" style="display:none" />
<div class="jfp-section">Priority (single upload slot)</div>
<div class="jfp-field">
<select id="jfp-file-priority" style="background:#16161a;border:1px solid #2a2a35;border-radius:6px;color:#e8e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:7px 9px;width:100%">
<option value="cv" ${settings.filePriority==='cv'?'selected':''}>CV (primary)</option>
<option value="resume" ${settings.filePriority==='resume'?'selected':''}>Resume (alternate)</option>
</select>
</div>
<button class="jfp-btn jfp-btn-primary" id="jfp-save-files">💾 Save File Preferences</button>
<div class="jfp-btn-row">
<button class="jfp-btn jfp-btn-secondary" id="jfp-clear-cv">🗑 CV</button>
<button class="jfp-btn jfp-btn-secondary" id="jfp-clear-resume">🗑 Resume</button>
</div>
</div>
<!-- SETTINGS TAB -->
<div class="jfp-pane" id="jfp-pane-settings" style="display:${activeTab==='settings'?'block':'none'}">
<div class="jfp-section">Behaviour</div>
<div class="jfp-toggle-row">
<div>
<div class="jfp-toggle-label">Auto-fill on page load</div>
<div class="jfp-toggle-desc">Fill forms as soon as an apply page opens</div>
</div>
<label class="jfp-toggle">
<input type="checkbox" id="jfp-auto-fill" ${settings.autoFill ? 'checked' : ''} />
<span class="jfp-slider"></span>
</label>
</div>
<div class="jfp-toggle-row">
<div>
<div class="jfp-toggle-label">Highlight filled fields</div>
<div class="jfp-toggle-desc">Green outline on auto-filled inputs</div>
</div>
<label class="jfp-toggle">
<input type="checkbox" id="jfp-highlight" ${settings.highlightFields !== false ? 'checked' : ''} />
<span class="jfp-slider"></span>
</label>
</div>
<button class="jfp-btn jfp-btn-primary" id="jfp-save-settings">💾 Save Settings</button>
<div class="jfp-divider"></div>
<div class="jfp-section">Data</div>
<div class="jfp-btn-row">
<button class="jfp-btn jfp-btn-secondary" id="jfp-export">↑ Export Profile</button>
<button class="jfp-btn jfp-btn-secondary" id="jfp-import">↓ Import Profile</button>
</div>
<button class="jfp-btn jfp-btn-stop" style="margin-top:7px" id="jfp-reset-all">🗑 Reset All Data</button>
</div>
</div>
</div>
</div>`;
}
function esc(s) { return (s || '').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
function formatSize(b) {
if (!b) return '';
if (b < 1024) return b + ' B';
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
return (b/1048576).toFixed(1) + ' MB';
}
function mount() {
const old = document.getElementById('jfp-root');
if (old) old.remove();
const div = document.createElement('div');
div.innerHTML = buildPanel();
document.body.appendChild(div.firstElementChild);
attachEvents();
}
function updatePanel() {
if (panelVisible) mount();
}
function togglePanel() {
panelVisible = !panelVisible;
const panel = document.getElementById('jfp-panel');
if (panel) panel.style.display = panelVisible ? 'flex' : 'none';
}
function switchTab(tab) {
activeTab = tab;
document.querySelectorAll('.jfp-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.querySelectorAll('.jfp-pane').forEach(p => p.style.display = 'none');
const pane = document.getElementById('jfp-pane-' + tab);
if (pane) pane.style.display = 'block';
}
function attachEvents() {
const $ = id => document.getElementById(id);
$('jfp-fab').onclick = togglePanel;
$('jfp-close').onclick = togglePanel;
document.querySelectorAll('.jfp-tab').forEach(t => {
t.onclick = () => switchTab(t.dataset.tab);
});
// Fill now
const fillNow = $('jfp-fill-now');
if (fillNow) fillNow.onclick = () => {
const result = fillPage();
showToast(`✅ Filled ${result.filled} fields, ${result.files} files`);
updatePanel();
};
// Submit
const submitBtn = $('jfp-submit-btn');
if (submitBtn) submitBtn.onclick = () => clickSubmit();
// Clear fields
const clearBtn = $('jfp-clear-btn');
if (clearBtn) clearBtn.onclick = () => {
document.querySelectorAll('input:not([type="hidden"]):not([type="file"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"]), textarea').forEach(el => {
if (!el.disabled && !el.readOnly) { el.value = ''; el.dispatchEvent(new Event('input', {bubbles:true})); }
});
showToast('🧹 Fields cleared');
};
// Scroll controls
const startBtn = $('jfp-start-scroll');
if (startBtn) startBtn.onclick = () => { startAutoScroll(); updatePanel(); };
const stopBtn = $('jfp-stop-scroll');
if (stopBtn) stopBtn.onclick = () => { stopAutoScroll(); updatePanel(); };
// Save scroll settings
const saveScroll = $('jfp-save-scroll-settings');
if (saveScroll) saveScroll.onclick = () => {
const s = Store.getSettings();
s.scrollDelay = parseInt($('jfp-scroll-delay')?.value) || 2500;
s.fillDelay = parseInt($('jfp-fill-delay')?.value) || 800;
s.autoSubmit = $('jfp-auto-submit')?.checked || false;
s.confirmSubmit = $('jfp-confirm-submit')?.checked !== false;
Store.setSettings(s);
showToast('✅ Scroll settings saved');
};
// Save profile
const saveProfile = $('jfp-save-profile');
if (saveProfile) saveProfile.onclick = () => {
Store.setProfile({
firstName: $('jfp-firstName')?.value.trim(),
lastName: $('jfp-lastName')?.value.trim(),
email: $('jfp-email')?.value.trim(),
phone: $('jfp-phone')?.value.trim(),
linkedin: $('jfp-linkedin')?.value.trim(),
street: $('jfp-street')?.value.trim(),
city: $('jfp-city')?.value.trim(),
zip: $('jfp-zip')?.value.trim(),
country: $('jfp-country')?.value.trim(),
coverLetter: $('jfp-coverLetter')?.value.trim(),
});
showToast('✅ Profile saved!');
Store.addLog('Profile updated', 'success');
};
// File uploads
const cvInput = $('jfp-cv-input');
if (cvInput) cvInput.onchange = (e) => handleFileUpload(e.target.files[0], 'cv');
const resumeInput = $('jfp-resume-input');
if (resumeInput) resumeInput.onchange = (e) => handleFileUpload(e.target.files[0], 'resume');
// Save file prefs
const saveFiles = $('jfp-save-files');
if (saveFiles) saveFiles.onclick = () => {
const s = Store.getSettings();
s.filePriority = $('jfp-file-priority')?.value || 'cv';
Store.setSettings(s);
showToast('✅ File preferences saved');
};
// Clear files
$('jfp-clear-cv')?.addEventListener('click', () => { Store.del('cvData'); Store.del('cvMeta'); showToast('CV cleared'); updatePanel(); });
$('jfp-clear-resume')?.addEventListener('click', () => { Store.del('resumeData'); Store.del('resumeMeta'); showToast('Resume cleared'); updatePanel(); });
// Settings
const saveSettings = $('jfp-save-settings');
if (saveSettings) saveSettings.onclick = () => {
const s = Store.getSettings();
s.autoFill = $('jfp-auto-fill')?.checked || false;
s.highlightFields = $('jfp-highlight')?.checked !== false;
Store.setSettings(s);
showToast('✅ Settings saved');
};
// Export
$('jfp-export')?.addEventListener('click', () => {
const data = { profile: Store.getProfile(), settings: Store.getSettings() };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'jobfiller-profile.json';
a.click();
showToast('📤 Profile exported');
});
// Import
$('jfp-import')?.addEventListener('click', () => {
const inp = document.createElement('input');
inp.type = 'file'; inp.accept = '.json';
inp.onchange = async (e) => {
try {
const text = await e.target.files[0].text();
const d = JSON.parse(text);
if (d.profile) Store.setProfile(d.profile);
if (d.settings) Store.setSettings(d.settings);
updatePanel();
showToast('✅ Profile imported');
} catch { showToast('❌ Invalid file'); }
};
inp.click();
});
// Reset
$('jfp-reset-all')?.addEventListener('click', () => {
if (!confirm('Reset ALL JobFiller Pro data?')) return;
GM_listValues().forEach(k => GM_deleteValue(k));
updatePanel();
showToast('🗑 All data cleared');
});
}
function handleFileUpload(file, type) {
if (!file) return;
if (file.size > 5 * 1024 * 1024) { showToast('❌ File too large (max 5MB)'); return; }
const reader = new FileReader();
reader.onload = (ev) => {
const b64 = ev.target.result.split(',')[1];
const meta = { name: file.name, size: file.size, type: file.type };
if (type === 'cv') { Store.setCVData(b64); Store.setCVMeta(meta); }
else { Store.setResumeData(b64); Store.setResumeMeta(meta); }
showToast(`✅ ${type.toUpperCase()} uploaded: ${file.name}`);
Store.addLog(`${type.toUpperCase()} uploaded: ${file.name}`, 'success');
updatePanel();
};
reader.readAsDataURL(file);
}
// ═══════════════════════════════════════════════════════════════════════════
// TAMPERMONKEY MENU COMMANDS
// ═══════════════════════════════════════════════════════════════════════════
GM_registerMenuCommand('⚡ Fill This Page', () => {
const r = fillPage();
showToast(`✅ Filled ${r.filled} fields, ${r.files} files`);
});
GM_registerMenuCommand('🔍 Start Auto-Scroll', () => startAutoScroll());
GM_registerMenuCommand('⏹ Stop Auto-Scroll', () => stopAutoScroll());
GM_registerMenuCommand('🪟 Toggle Panel', () => { panelVisible = !panelVisible; updatePanel(); });
// ═══════════════════════════════════════════════════════════════════════════
// AUTO-FILL ON LOAD
// ═══════════════════════════════════════════════════════════════════════════
const JOB_APPLY_PATTERN = /\/(apply|application|job|position|prihlasit|prihlaska|reagovat|odpovedet|nabidka)/i;
function init() {
mount();
// Check if we're returning from a scroll session
const wasScrolling = Store.get('scrollActive', false);
if (wasScrolling) {
scrollActive = true;
scrollQueue = Store.get('scrollQueue', []);
scrollIndex = Store.get('scrollIndex', 0);
Store.set('scrollActive', false); // clear flag; resumeScrollMode sets it again when needed
resumeScrollMode();
return;
}
// Auto-fill on apply pages
const settings = Store.getSettings();
if (settings.autoFill && JOB_APPLY_PATTERN.test(location.pathname)) {
setTimeout(() => {
const r = fillPage();
if (r.filled + r.files > 0) showToast(`⚡ Auto-filled ${r.filled} fields, ${r.files} files`);
}, 1000);
}
}
// Wait for DOM to be ready enough
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();