Last active
November 14, 2024 02:17
-
-
Save azrafe7/020c8e3fe00f0bd2b70f283a7de8513c to your computer and use it in GitHub Desktop.
Navigate Upwork jobs with the keyboard
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
| "use strict"; | |
| (() => { | |
| let currCard = null; | |
| let listeners = []; | |
| let observer = null; | |
| const AUTO_LOAD_DETAILS = false; | |
| const NEWER_JOBS_BUTTON_SELECTOR = 'button[data-test="newer-jobs-button"]'; | |
| const BUTTON_SELECTOR = 'button[data-ev-label="pagination_next_page"], button[data-test="load-more-button"]'; | |
| const CARD_SELECTOR = 'div[data-test="job-tile-list"] section.air3-card-section, article[data-ev-label="search_results_impression"]'; | |
| const LINK_SELECTOR = 'a[data-test*="job-tile-title-link"], a.air3-link'; | |
| const PAYMENT_VERIFIED_SELECTOR = '*[data-test="payment-verification-status"], *[data-test="payment-verified"]'; | |
| function addStyles() { | |
| var style = document.createElement('style'); | |
| style.type = 'text/css'; | |
| style.innerHTML = ` | |
| /* fixed price */ | |
| li[data-test="is-fixed-price"] strong:last-child, | |
| span[data-test="budget"] { | |
| background-color: rgb(0, 200, 0); | |
| color: white; | |
| border-radius: 8px; | |
| padding: 0px 2px; | |
| text-shadow: 1px 1px 1px #555; | |
| } | |
| /* visited card links' color */ | |
| a[data-test*="job-tile-title-link"]:visited, a.air3-link:visited { | |
| color: rgb(201, 148, 86) !important; | |
| } | |
| /* disable outline and text-decoration on keyboard-user :focus */ | |
| body.keyboard-user :focus { | |
| outline: none !important; | |
| text-decoration: none !important; | |
| } | |
| /* saved jobs icon color */ | |
| button[aria-label^="Save job"] { | |
| color: rgb(230, 0, 0); | |
| } | |
| .highlight.selected, | |
| button[data-test="newer-jobs-button"]:focus, | |
| button[data-ev-label="pagination_next_page"]:focus, | |
| button[data-test="load-more-button"]:focus { | |
| background-color: rgb(234 196 48); | |
| color: #fff !important; | |
| border-color: #ebb83f !important; | |
| border-width: 1px; | |
| border-style: solid; | |
| border-radius: 3px; | |
| text-shadow: 1px 1px 1px #555; | |
| } | |
| .highlight.selected:before, | |
| button[data-test="newer-jobs-button"]:focus:before, | |
| button[data-ev-label="pagination_next_page"]:focus:before, | |
| button[data-test="load-more-button"]:focus:before { | |
| content: "► "; | |
| padding-left: 8px; | |
| padding-right: 1px; | |
| } | |
| .fading-message { | |
| position: fixed; | |
| z-index: 9999; | |
| font-size: 1.1em; | |
| left: 5%; | |
| top: 10px; | |
| width: 90%; | |
| text-align: center; | |
| background-color: rgb(234 196 48 / 80%); | |
| padding: 8px 8px; | |
| vertical-align: middle; | |
| font-family: monospace; | |
| color: #fff; | |
| text-shadow: 1px 1px 1px #555; | |
| border-radius: 3px; | |
| border-color: #ebb83f; | |
| border-width: 1px; | |
| border-style: solid; | |
| } | |
| .text-shadow { | |
| text-shadow: 1px 1px 0px #555; | |
| } | |
| .uwn-details { | |
| line-height: 1em; | |
| opacity: 0; | |
| } | |
| .uwn-detail { | |
| font-size: 12px; | |
| margin-right: 7px; | |
| } | |
| .uwn-detail-key { | |
| font-weight: 300; | |
| } | |
| .uwn-detail-value { | |
| font-weight: 500; | |
| } | |
| `; | |
| document.getElementsByTagName('head')[0].appendChild(style); | |
| } | |
| function removeFadeOut(el, delay, ease='ease') { | |
| el.style.transition = "opacity " + delay + "ms " + ease; | |
| el.style.opacity = 0; | |
| setTimeout(function() { | |
| el.remove(); | |
| }, delay); | |
| } | |
| function fadingMessage(msg, fadeDelay=5000, ease='ease-in') { | |
| let messageElems = document.querySelectorAll('.fading-message'); | |
| messageElems.forEach((m) => m.remove()); | |
| let messageElem = document.createElement('div'); | |
| messageElem.innerHTML = msg; | |
| console.log(msg); | |
| messageElem.classList.add('fading-message'); | |
| document.documentElement.appendChild(messageElem); | |
| setTimeout(() => removeFadeOut(messageElem, fadeDelay, ease), 100); | |
| } | |
| function deleteOldCards() { | |
| let cards = Array.from(document.querySelectorAll(CARD_SELECTOR)); | |
| let numCardsToDelete = Math.max(cards.length - 19, 0); | |
| for (let i = 0; i < numCardsToDelete; i++) cards[i + 1].remove(); /* keep first card */ | |
| return numCardsToDelete; | |
| } | |
| function parseDetailsResponse(dom, card) { | |
| let details = {}; | |
| let matches = null; | |
| let numAttachments = 0; | |
| let attachmentHeaders = Array.from(dom.documentElement.querySelectorAll('h5 span')); | |
| if (attachmentHeaders.length > 0) { | |
| numAttachments = attachmentHeaders[0].parentElement.parentElement.querySelectorAll('li').length; | |
| } | |
| details["attachments"] = numAttachments; | |
| if (card) { | |
| const httpUrlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; | |
| const urlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g; | |
| let numLinks = 0; | |
| let jobText = card.querySelector('*[data-test="job-description-text"]').innerText; | |
| matches = Array.from(jobText.matchAll(urlRegex)); | |
| if (matches.length > 0) { | |
| numLinks = matches.length; | |
| } | |
| details["links"] = numLinks; | |
| } | |
| /* let aboutText = dom.querySelector('div[data-test="AboutClientUser"]').innerText; */ | |
| let aboutText = dom.querySelector('div.sidebar').innerText; | |
| let aboutRegexes = { | |
| "connects needed": /(a proposal:\s+(\d+)|proposal for:\s+(\d+))/gi, | |
| "jobs posted": /(\d+) jobs? posted/gi, | |
| "hire rate": /(\d+%) hire rate/gi, | |
| "open jobs": /(\d+) open job/gi, | |
| }; | |
| for (let [k, v] of Object.entries(aboutRegexes)) { | |
| details[k] = null; | |
| matches = Array.from(aboutText.matchAll(v)); | |
| /* console.log(k, matches); */ | |
| if (k === 'connects needed') { | |
| details[k] = matches.length > 0 ? (matches[0][2] || matches[0][3]) : null; | |
| } else { | |
| details[k] = matches.length > 0 ? matches[0][1] : null; | |
| } | |
| if ((k === 'connects needed' || k === 'hire rate') && details[k] == null) { | |
| /* debugger; */ | |
| fadingMessage("failed to extract 'connects needed' or 'hire rate'"); | |
| if (card != null) card.scrollIntoViewIfNeeded(); | |
| } | |
| } | |
| let activityCard = dom.querySelector('section.air3-card-section.air3-grid-container'); | |
| const proposalsRegex = /\s*((\d+)\D+(\d+)|(\D+(\d+))|(\d+)\+)/gi; | |
| let activityTitles = { | |
| "proposals": "proposals", | |
| "invites sent": "invites sent", | |
| "unanswered invites": "unanswered invites", | |
| "interviewing": "interviewing", | |
| "last viewed": "last viewed", | |
| "hires": "hires", | |
| }; | |
| let titleElems = Array.from(activityCard.querySelectorAll('.title')); | |
| for (let [k, v] of Object.entries(activityTitles)) { | |
| details[k] = null; | |
| const filteredElems = titleElems.filter((e) => e.innerText.toLowerCase().includes(v)); | |
| if (filteredElems.length == 0) continue; | |
| const titleElem = filteredElems[0]; | |
| const valueElem = titleElem.parentElement.querySelector('.value'); | |
| if (k === 'proposals') { | |
| matches = Array.from(valueElem.innerText.matchAll(proposalsRegex)); | |
| if (matches.length > 0) { | |
| matches = matches[0]; | |
| if (matches[6] != null) { | |
| details[k] = matches[6] + '+'; | |
| } else if (matches[2] != null && matches[3] != null) { | |
| details[k] = matches[2] + ' to ' + matches[3]; | |
| } else if (matches[5] != null) { | |
| details[k] = '0 to ' + matches[5]; | |
| } | |
| } | |
| } else { | |
| details[k] = valueElem.innerText.trim(); | |
| } | |
| } | |
| return details; | |
| } | |
| /* parseDetailsResponse(document); */ | |
| const PERCENT_COLORS = [ | |
| {percent:0.00, color:{r:0xf0, g:0x00, b:0}}, | |
| {percent:0.70, color:{r:0xf0, g:0xf0, b:0}}, | |
| {percent:1.00, color:{r:0x00, g:0xf0, b:0}} | |
| ]; | |
| /* percent in range [0,1] */ | |
| function getColorForPercentage(percent) { | |
| let i = 1; | |
| for (i = 1; i < PERCENT_COLORS.length - 1; i++) { | |
| if (percent < PERCENT_COLORS[i].percent) { | |
| break; | |
| } | |
| } | |
| let lower = PERCENT_COLORS[i - 1]; | |
| let upper = PERCENT_COLORS[i]; | |
| let range = upper.percent - lower.percent; | |
| let rangePercent = (percent - lower.percent) / range; | |
| let percentLower = 1 - rangePercent; | |
| let percentUpper = rangePercent; | |
| let color = { | |
| r: Math.floor(lower.color.r * percentLower + upper.color.r * percentUpper), | |
| g: Math.floor(lower.color.g * percentLower + upper.color.g * percentUpper), | |
| b: Math.floor(lower.color.b * percentLower + upper.color.b * percentUpper) | |
| }; | |
| return 'rgb(' + [color.r, color.g, color.b].join(',') + ')'; | |
| } | |
| function removeCardDetails(card) { | |
| card.querySelectorAll('.uwn-details').forEach((el) => el.remove()); | |
| } | |
| function addDetailsToCard(card, details, fields, add=false) { | |
| const detailsElem = document.createElement('div'); | |
| detailsElem.classList.add('uwn-details'); | |
| let jobInfoElem = card.querySelector('ul[data-test="JobInfoClient"]'); | |
| if (jobInfoElem) { | |
| jobInfoElem.style.setProperty('margin-bottom', '0px', 'important'); | |
| } else { | |
| jobInfoElem = card.querySelector('*[data-test="job-type"]').parentElement.parentElement.parentElement.previousElementSibling; | |
| } | |
| jobInfoElem = card.querySelector('.uwn-details') || jobInfoElem; | |
| jobInfoElem.insertAdjacentElement('afterEnd', detailsElem); | |
| const spanTemplate = ` | |
| <span class="uwn-detail"> | |
| <span class="uwn-detail-key">$key: | |
| </span> | |
| <span class="uwn-detail-value">$value | |
| </span> | |
| </span> | |
| `; | |
| const MIN_CONNECTS = 2; | |
| const MAX_CONNECTS = 28; | |
| const CONNECTS_FACTOR = 100 / (MAX_CONNECTS - MIN_CONNECTS); | |
| let tempEl = document.createElement('div'); | |
| if (fields == null) fields = Object.keys(details); | |
| for (let k of fields) { | |
| let addTextShadow = false; | |
| let v = details[k]; | |
| if (v == null) continue; | |
| tempEl.innerHTML = spanTemplate.replace('$key', k).replace('$value', v != null ? v : ''); | |
| let clonedNode = tempEl.firstElementChild.cloneNode(true); | |
| if (k === 'hire rate') { | |
| clonedNode.lastElementChild.style.color = getColorForPercentage(parseInt(v) / 100); | |
| addTextShadow = true; | |
| } | |
| if (k === 'connects needed') { | |
| const connectsPercentage = 100 - (parseInt(v) - 3) * CONNECTS_FACTOR; | |
| clonedNode.lastElementChild.style.color = getColorForPercentage(connectsPercentage / 100); | |
| /* clonedNode.lastElementChild.innerHTML += '/' + connectsPercentage; */ | |
| addTextShadow = true; | |
| } | |
| if (k === 'attachments' && v > 0) { | |
| clonedNode.lastElementChild.style.color = 'rgb(13, 148, 225)'; | |
| addTextShadow = true; | |
| } | |
| if (k === 'links' && v > 0) { | |
| clonedNode.lastElementChild.style.color = 'rgb(13, 148, 225)'; | |
| addTextShadow = true; | |
| } | |
| if (k === 'hires' && v > 0) { | |
| clonedNode.lastElementChild.style.color = 'rgb(200, 0, 0)'; | |
| addTextShadow = true; | |
| } | |
| if (k === 'invites sent' && v > 0) { | |
| clonedNode.lastElementChild.style.color = 'orangered'; | |
| addTextShadow = true; | |
| } | |
| if (addTextShadow) { | |
| clonedNode.lastElementChild.classList.add('text-shadow'); | |
| } | |
| detailsElem.appendChild(clonedNode); | |
| detailsElem.style.transition = "opacity 500ms ease"; | |
| setTimeout(() => detailsElem.style.opacity = 1, 10); | |
| } | |
| } | |
| function getAllCards() { | |
| let cards = Array.from(document.querySelectorAll(CARD_SELECTOR)); | |
| return cards; | |
| } | |
| function parseAndAddDetailsToCard(card, quiet=false) { | |
| const href = card.querySelector(LINK_SELECTOR).getAttribute("href"); | |
| if (!quiet) fadingMessage("fetching details..."); | |
| try { | |
| fetch(href).then((response) => { | |
| if (!response.ok) { | |
| throw new Error('Error: ' + response.status); | |
| } else { | |
| response.text().then((text) => { | |
| if (!quiet) fadingMessage('fetched'); | |
| try { | |
| const parser = new DOMParser(); | |
| let dom = parser.parseFromString(text, "text/html"); | |
| let details = parseDetailsResponse(dom, card); | |
| console.log("details", details); | |
| removeCardDetails(card); | |
| addDetailsToCard(card, details, ["connects needed", "attachments", "links", "hire rate", "proposals", "last viewed"]); | |
| addDetailsToCard(card, details, ["hires", "interviewing", "invites sent", "unanswered invites", "open jobs", "jobs posted"]); | |
| } catch (e) { | |
| fadingMessage("Error parsing details: " + e.message); | |
| console.error(e); | |
| } | |
| }); | |
| } | |
| }); | |
| } catch (e) { | |
| fadingMessage(e.message); | |
| console.error(e); | |
| } | |
| } | |
| function clamp(x, min, max) { | |
| return Math.max(min, Math.min(x, max)); | |
| } | |
| function highlightCard(cardOrCardIndex) { | |
| let cards = getAllCards(); | |
| cards.forEach((c) => { | |
| const link = c.querySelector(LINK_SELECTOR); | |
| link.classList.toggle('highlight', false); | |
| link.classList.toggle('selected', false); | |
| }); | |
| let card = (typeof cardOrCardIndex === 'number') ? cards[clamp(cardOrCardIndex, 0, cards.length - 1)] : cardOrCardIndex; | |
| let currLink = card.querySelector(LINK_SELECTOR); | |
| currLink.classList.toggle('highlight', true); | |
| currLink.classList.toggle('selected', true); | |
| console.log('▷ ' + card.querySelector('.job-tile-title').textContent); | |
| card.scrollIntoViewIfNeeded(); | |
| currLink.focus(); | |
| currCard = card; | |
| } | |
| function handleKeyDown(e) { | |
| let key = e.key.toLowerCase(); | |
| let button = document.querySelector(BUTTON_SELECTOR); | |
| let newerJobsButton = document.querySelector(NEWER_JOBS_BUTTON_SELECTOR); | |
| if (button && e.ctrlKey && key == 'arrowdown') { | |
| button.scrollIntoViewIfNeeded(); | |
| button.focus(); | |
| } | |
| if (newerJobsButton && e.ctrlKey && key == 'arrowup') { | |
| newerJobsButton.scrollIntoViewIfNeeded(); | |
| newerJobsButton.focus(); | |
| } | |
| if (currCard && ((e.ctrlKey && key == ' ') || (e.shiftKey && key == '_'))) { | |
| parseAndAddDetailsToCard(currCard); | |
| return; | |
| } | |
| if (currCard && (key == '+' || key == '-' || key == '*')) { | |
| let moreLessButton = currCard.querySelector('button[data-ev-open]'); | |
| if (moreLessButton) { | |
| const isOpen = moreLessButton.getAttribute('data-ev-open') == 'true'; | |
| if (isOpen && key == '-') moreLessButton.click(); | |
| if (!isOpen && key == '+') moreLessButton.click(); | |
| if (key == '*') moreLessButton.click(); | |
| } | |
| } | |
| if (e.shiftKey && (key == 'home' || key == 'end' || key == 'pageup' || key == 'pagedown')) { | |
| if (key == 'home') { | |
| highlightCard(0); | |
| } else { | |
| const cards = getAllCards(); | |
| if (key == 'end') highlightCard(cards.length - 1); | |
| if (currCard) { | |
| const cardIndex = cards.indexOf(currCard); | |
| if (key == 'pageup') highlightCard(cardIndex - 10); | |
| if (key == 'pagedown') highlightCard(cardIndex + 10); | |
| } | |
| } | |
| } | |
| if (e.shiftKey && (key == 'arrowdown' || key == 'arrowup')) { | |
| let noCardSelected = !currCard || !currCard.checkVisibility(); | |
| if (noCardSelected) currCard = document.querySelector(CARD_SELECTOR); | |
| if (currCard) { | |
| let otherCard = null; | |
| if (noCardSelected) { | |
| otherCard = currCard; | |
| } else { | |
| if (key == 'arrowdown') { | |
| if (button && !currCard.nextElementSibling) { | |
| button.scrollIntoViewIfNeeded(); | |
| if (document.activeElement == button) { | |
| button.click(); | |
| } | |
| button.focus(); | |
| return; /* return early */ | |
| } | |
| otherCard = currCard.nextElementSibling; | |
| } | |
| if (key == 'arrowup') { | |
| if (newerJobsButton && !currCard.previousElementSibling) { | |
| newerJobsButton.scrollIntoViewIfNeeded(); | |
| if (document.activeElement == newerJobsButton) { | |
| newerJobsButton.click(); | |
| } | |
| newerJobsButton.focus(); | |
| return; /* return early */ | |
| } | |
| otherCard = currCard.previousElementSibling; | |
| } | |
| if ((button && document.activeElement == button) || (newerJobsButton && document.activeElement == newerJobsButton)) { | |
| otherCard = currCard; | |
| } | |
| if (!otherCard) { | |
| currCard.scrollIntoViewIfNeeded(); | |
| } | |
| } | |
| if (otherCard) { | |
| highlightCard(otherCard); | |
| } | |
| } | |
| } | |
| if (e.ctrlKey && e.key.toLowerCase() == "backspace") { | |
| let numCardsDeleted = deleteOldCards(); | |
| fadingMessage(`deleted ${numCardsDeleted} old cards`); | |
| } | |
| if (e.ctrlKey && e.key.toLowerCase() == "arrowright") { | |
| button = document.querySelector(BUTTON_SELECTOR); | |
| if (button) { | |
| let res = prompt("Load how many pages?", 5); | |
| if (res != null) { | |
| let nPages = parseInt(res); | |
| if (!isNaN(nPages) && nPages >= 0) { | |
| fadingMessage(`start loading ${nPages} more pages...`); | |
| loadMorePages(nPages, false); | |
| } else { | |
| fadingMessage(`invalid number: '${res}'`); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function removeEventListeners() { | |
| for (let listener of listeners) { | |
| document.removeEventListener(listener.type, listener.handler); | |
| } | |
| listeners.length = 0; | |
| } | |
| function addEventListeners() { | |
| document.addEventListener("keydown", handleKeyDown); | |
| listeners.push({type:"keydown", handler: handleKeyDown}); | |
| } | |
| function loadMorePages(numPages, scrollToButton=false) { | |
| let timeout = null; | |
| let delay = 400; | |
| const pagesToLoad = numPages; | |
| let retries = 3; | |
| let scrollTop = document.documentElement.scrollTop; | |
| let bound_function = () => _loadMorePages(scrollToButton); | |
| function _loadMorePages(scrollToButton) { | |
| let button = document.querySelector(BUTTON_SELECTOR); | |
| if (button == null) { | |
| if (retries > 0) { | |
| timeout = setTimeout(bound_function, 1000); | |
| retries--; | |
| } else { | |
| fadingMessage('next page button not found'); | |
| } | |
| } | |
| console.log("loadMorePages", numPages, button.getAttribute('disabled'), 'scroll:' + scrollToButton); | |
| if (scrollToButton) { | |
| console.log("scrollToButton"); | |
| button.scrollIntoViewIfNeeded(); | |
| } else { | |
| console.log("NO scrollToButton"); | |
| console.log("scroll to:", scrollTop); | |
| document.documentElement.scrollTop = scrollTop; | |
| } | |
| if (numPages > 0) { | |
| if (button.getAttribute('disabled') == null) { | |
| console.log("buttonClick"); | |
| scrollTop = document.documentElement.scrollTop; | |
| button.click(); | |
| /* button.dispatchEvent(new Event("click")); */ | |
| numPages--; | |
| } | |
| timeout = setTimeout(bound_function, delay); | |
| } else { | |
| if (numPages == 0) { | |
| if (button.getAttribute('disabled')) { /* wait for it to scrollIntoViewIfNeeded */ | |
| console.log("waitForScrollIntoView"); | |
| timeout = setTimeout(bound_function, delay); | |
| } else { | |
| console.log("clearTimeout"); | |
| clearTimeout(timeout); | |
| fadingMessage(`finished loading ${pagesToLoad} more pages`); | |
| } | |
| } | |
| } | |
| } | |
| bound_function(); | |
| } | |
| /** | |
| * elementsReady() reworked from jwilson8767 elementReady() code | |
| * | |
| * MIT Licensed | |
| * Authors: jwilson8767, azrafe7 | |
| * | |
| */ | |
| function elementsReady(selectors, callback, options={}) { | |
| let dummyObserver = { | |
| disconnect: function() { | |
| console.log('[dummyObserver] Already disconnected!'); | |
| } | |
| }; | |
| const defaults = {once:false, root:document.documentElement, filterFn:(elements) => {return true}}; | |
| options = {...defaults, ...options}; | |
| const root = options.root; | |
| const filterFn = options.filterFn; | |
| if (!Array.isArray(selectors)) selectors = [selectors]; | |
| let matchedSelectors = selectors.map(() => false); | |
| const query = (msg) => { | |
| console.log(msg); | |
| for (const [index, selector] of selectors.entries()) { | |
| const alreadyMatched = matchedSelectors[index]; | |
| if (options.once && alreadyMatched) continue; | |
| let elements = Array.from(root.querySelectorAll(selector)); | |
| if (elements.length > 0) { | |
| matchedSelectors[index] = true; | |
| } | |
| if (filterFn) { | |
| elements = elements.filter(filterFn); | |
| } | |
| if (elements.length > 0) { | |
| console.log('index', index); | |
| callback(elements, selector, index, matchedSelectors); | |
| } | |
| } | |
| }; | |
| query("query from function"); | |
| if (options.once && matchedSelectors.every((matched) => matched)) { | |
| console.log("ALL"); | |
| return dummyObserver; | |
| } | |
| let mutObserver = new MutationObserver((mutationRecords, observer) => { | |
| query("query from observer"); | |
| if (options.once && matchedSelectors.every((matched) => matched)) { | |
| console.log("ALL"); | |
| observer.disconnect(); | |
| return dummyObserver; | |
| } | |
| }); | |
| mutObserver.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true, | |
| /*attributes: true, | |
| attibuteFilter: ['class']*/ | |
| }); | |
| return mutObserver; | |
| } | |
| function cardObserverCallback(elements, selector, index, matchedSelectors) { | |
| console.log('callback called'); | |
| console.log(elements, selector, index, matchedSelectors); | |
| if (index == 0) { | |
| for (let elem of elements) { | |
| let paymentVerifiedElem = elem.querySelector(PAYMENT_VERIFIED_SELECTOR); | |
| let paymentText = paymentVerifiedElem && paymentVerifiedElem.innerText; | |
| if (paymentText) { | |
| let children = paymentVerifiedElem.querySelectorAll('*'); | |
| if (paymentText.includes('unverified')) { | |
| console.log('unverified'); | |
| children.forEach((el) => el.style.setProperty('color', 'orangered', 'important')); | |
| } else { | |
| console.log('verified'); | |
| children.forEach((el) => el.style.setProperty('color', 'rgb(0, 200, 0)', 'important')); | |
| } | |
| } | |
| if (AUTO_LOAD_DETAILS) parseAndAddDetailsToCard(elem, true); | |
| elem.classList.add('uwn-already-observed'); | |
| } | |
| } else { | |
| elements[0].classList.add('uwn-already-observed'); | |
| fadingMessage('newer jobs available'); | |
| } | |
| } | |
| function startObserver() { | |
| observer = elementsReady([CARD_SELECTOR, NEWER_JOBS_BUTTON_SELECTOR], cardObserverCallback, {once:false, filterFn: (elem) => !elem.classList.contains('uwn-already-observed')}); | |
| } | |
| function parseAndAddDetailsToAllCards(forceUpdate=false, quiet=false) { | |
| let cards = getAllCards(); | |
| cards.forEach((c) => { | |
| if (forceUpdate || c.querySelector('.uwn-details') == null) { | |
| parseAndAddDetailsToCard(c, quiet); | |
| } | |
| }); | |
| } | |
| function exportModule() { | |
| let UWNext = { | |
| currCard: currCard, | |
| listeners: listeners, | |
| observer: observer, | |
| AUTO_LOAD_DETAILS: AUTO_LOAD_DETAILS, | |
| NEWER_JOBS_BUTTON_SELECTOR: NEWER_JOBS_BUTTON_SELECTOR, | |
| BUTTON_SELECTOR: BUTTON_SELECTOR, | |
| CARD_SELECTOR: CARD_SELECTOR, | |
| LINK_SELECTOR: LINK_SELECTOR, | |
| PAYMENT_VERIFIED_SELECTOR: PAYMENT_VERIFIED_SELECTOR, | |
| addStyles: addStyles, | |
| removeFadeOut: removeFadeOut, | |
| fadingMessage: fadingMessage, | |
| deleteOldCards: deleteOldCards, | |
| parseDetailsResponse: parseDetailsResponse, | |
| PERCENT_COLORS: PERCENT_COLORS, | |
| getColorForPercentage: getColorForPercentage, | |
| removeCardDetails: removeCardDetails, | |
| addDetailsToCard: addDetailsToCard, | |
| getAllCards: getAllCards, | |
| parseAndAddDetailsToCard: parseAndAddDetailsToCard, | |
| highlightCard: highlightCard, | |
| handleKeyDown: handleKeyDown, | |
| removeEventListeners: removeEventListeners, | |
| addEventListeners: addEventListeners, | |
| loadMorePages: loadMorePages, | |
| elementsReady: elementsReady, | |
| parseAndAddDetailsToAllCards: parseAndAddDetailsToAllCards, | |
| startObserver: startObserver, | |
| startUWNext: startUWNext, | |
| }; | |
| window.UWNext = UWNext; | |
| } | |
| function startUWNext() { | |
| addStyles(); | |
| addEventListeners(); | |
| fadingMessage('upwork-next injected'); | |
| startObserver(); | |
| exportModule(); | |
| } | |
| startUWNext(); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
SHIFT + (UP | DOWN): next/prev job
CTRL + DOWN: load more
default ENTER / CTRL + ENTER: once focused, go to next page/open job/etc.