|
// ==UserScript== |
|
// @name Skeb Helper |
|
// @namespace https://skeb.jp/ |
|
// @version 2025-04-23 |
|
// @description Helpful tools for Skeb |
|
// @author CJMAXiK |
|
// @license All Rights Reserved |
|
// @homepage https://cjmaxik.com/skeb-helper |
|
// @match https://skeb.jp/* |
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=skeb.jp |
|
// @downloadURL https://gist.github.com/cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605/raw/script.user.js |
|
// @updateURL https://gist.github.com/cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605/raw/script.user.js |
|
// @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config/gm_config.min.js |
|
// @run-at document-start |
|
// @grant GM_xmlhttpRequest |
|
// @grant GM_getValue |
|
// @grant GM_setValue |
|
// @grant GM_deleteValue |
|
// @grant GM_addStyle |
|
// @connect cdn.jsdelivr.net |
|
// ==/UserScript== |
|
|
|
"use strict"; |
|
|
|
let currency; |
|
let rates = {}; |
|
|
|
let currentPageUrl; |
|
let creatorCache = new Map(); |
|
let worksCache = new Set(); |
|
let currentAccount; |
|
|
|
const apiEndpoint = "https://skeb.jp/api"; |
|
|
|
const convertValutesSettings = (data) => { |
|
const valutesLines = data.split("\n"); |
|
|
|
let valutes = {}; |
|
try { |
|
valutes = valutesLines.map((line) => { |
|
const values = line.split(","); |
|
|
|
if ((values.length !== 1 && values.length !== 2) || !values[1]) { |
|
throw new Error( |
|
`The valute line should have 1 or 2 values split by commas; supplied ${values.length} instead.` |
|
); |
|
} |
|
|
|
return { |
|
valute: values[0].trim(), |
|
correction: Number(values[1].trim() ?? 1.0), |
|
}; |
|
}); |
|
} catch (e) { |
|
console.error(e); |
|
} |
|
|
|
return valutes; |
|
}; |
|
|
|
const convertPriceAlerts = (data) => { |
|
const alertLine = data.trim(); |
|
|
|
let alerts = {}; |
|
try { |
|
const alertsArray = alertLine.split(","); |
|
|
|
if (alertsArray.length !== 3) { |
|
throw new Error( |
|
`The alert line should have 3 values split by commas; supplied ${alertsArray.length} instead.` |
|
); |
|
} |
|
|
|
alerts = { |
|
valute: alertsArray[0].trim(), |
|
warning: Number(alertsArray[1].trim()), |
|
danger: Number(alertsArray[2].trim()), |
|
}; |
|
} catch (e) { |
|
console.error(e); |
|
} |
|
|
|
return alerts; |
|
}; |
|
|
|
// Default settings |
|
const defaultValutes = "rub, 1.05\ntry, 1"; |
|
const defaultAlerts = "rub, 2500, 5000"; |
|
let settings = { |
|
blurNsfw: false, |
|
conversionEnabled: false, |
|
conversionValutes: convertValutesSettings(defaultValutes), |
|
priceAlerts: convertPriceAlerts(defaultAlerts), |
|
debug: false, |
|
}; |
|
|
|
// MIGRATION: We are using localStorage instead |
|
GM_deleteValue("works"); |
|
|
|
let frame = document.createElement("div"); |
|
let gms = new GM_config({ |
|
id: "SkebHelperConfig", |
|
title: "Skeb Helper Settings", |
|
fields: { |
|
conversionEnabled: { |
|
label: "Enable", |
|
section: ["Currency Conversion & Price Alerts"], |
|
type: "checkbox", |
|
default: false, |
|
}, |
|
debug: { |
|
label: "Debug (enable only if asked by the developer)", |
|
type: "checkbox", |
|
default: true, |
|
}, |
|
conversionValutes: { |
|
label: |
|
'New line for every currency, <span class="tag">currency, correction</span>', |
|
section: ["Conversion Currencies"], |
|
type: "textarea", |
|
default: defaultValutes, |
|
}, |
|
Button: { |
|
label: "Open the list of currencies in a new tab", |
|
type: "button", |
|
size: 100, |
|
click: function () { |
|
window |
|
.open( |
|
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies.json", |
|
"_blank" |
|
) |
|
.focus(); |
|
}, |
|
}, |
|
Button2: { |
|
label: "Restore the default values", |
|
type: "button", |
|
size: 100, |
|
click: function () { |
|
document.getElementById( |
|
"SkebHelperConfig_field_conversionValutes" |
|
).value = defaultValutes; |
|
}, |
|
}, |
|
//// |
|
priceAlerts: { |
|
label: |
|
'Use this to visually alert yourself on the price of the commission in your currency, <span class="tag">currency, warning, danger</span>', |
|
section: ["Price Alerts"], |
|
type: "textarea", |
|
default: defaultAlerts, |
|
}, |
|
Button3: { |
|
label: "Restore the default values", |
|
type: "button", |
|
size: 100, |
|
click: function () { |
|
document.getElementById("SkebHelperConfig_field_priceAlerts").value = |
|
defaultAlerts; |
|
}, |
|
}, |
|
//// |
|
blurNsfw: { |
|
label: "Blur NSFW works", |
|
section: ["NSFW settings"], |
|
type: "checkbox", |
|
default: true, |
|
}, |
|
Button4: { |
|
label: "Clear Cache", |
|
section: ["Other settings"], |
|
type: "button", |
|
size: 100, |
|
click: function () { |
|
if ( |
|
confirm( |
|
"Do you want to clear the cache?\n\nUse this function only if absolutely necessary." |
|
) |
|
) { |
|
window.localStorage.removeItem("helper-works"); |
|
window.location.reload(); |
|
} |
|
}, |
|
}, |
|
Button5: { |
|
label: "Helper's Official Page", |
|
type: "button", |
|
size: 100, |
|
click: function () { |
|
window.open("https://cjmaxik.com/skeb-helper", "_blank").focus(); |
|
}, |
|
}, |
|
}, |
|
|
|
events: { |
|
init: function () { |
|
const conversionEnabled = this.get("conversionEnabled"); |
|
settings = { |
|
blurNsfw: this.get("blurNsfw"), |
|
conversionEnabled, |
|
conversionValutes: convertValutesSettings( |
|
this.get("conversionValutes") |
|
), |
|
priceAlerts: convertPriceAlerts(this.get("priceAlerts")), |
|
debug: this.get("debug"), |
|
}; |
|
}, |
|
|
|
open: function (doc) { |
|
doc.querySelectorAll("textarea").forEach((element) => { |
|
element.classList.add( |
|
"textarea", |
|
"is-static", |
|
"is-underline", |
|
"is-p-8" |
|
); |
|
}); |
|
|
|
doc.querySelectorAll("checkbox").forEach((element) => { |
|
element.classList.add("checkbox"); |
|
}); |
|
|
|
doc.querySelectorAll("button, input[type=button]").forEach((element) => { |
|
element.classList.add("button", "is-primary"); |
|
}); |
|
}, |
|
|
|
save: function () { |
|
window.location.reload(); |
|
}, |
|
}, |
|
|
|
css: ` |
|
#SkebHelperConfig * { font-family: inherit !important; } |
|
#SkebHelperConfig textarea { field-sizing: content; } |
|
#SkebHelperConfig .field_label { font-size: 1em !important; font-weight: 400 !important; line-height: 1.5 !important; } |
|
#SkebHelperConfig #SkebHelperConfig_wrapper { padding: 10px; } |
|
#SkebHelperConfig .section_header_holder { margin-top: 2rem !important; } |
|
#SkebHelperConfig .tag { font-size: inherit !important; } |
|
`, |
|
frame, |
|
}); |
|
|
|
const print_debug = (...text) => { |
|
if (!settings.debug) return; |
|
console.debug(...text); |
|
}; |
|
|
|
// Intercept all requests |
|
(function (open) { |
|
window.XMLHttpRequest.prototype.open = function () { |
|
this.addEventListener( |
|
"readystatechange", |
|
function () { |
|
if (this.readyState == 4) { |
|
intercept(this); |
|
} |
|
}, |
|
false |
|
); |
|
open.apply(this, arguments); |
|
}; |
|
})(window.XMLHttpRequest.prototype.open); |
|
|
|
const makeRequest = (url, additional_headers) => { |
|
return new Promise((resolve, reject) => { |
|
GM_xmlhttpRequest({ |
|
method: "GET", |
|
url, |
|
headers: { |
|
"Content-Type": "application/json", |
|
...additional_headers, |
|
}, |
|
onload: function (response) { |
|
resolve(response.responseText); |
|
}, |
|
onerror: function (error) { |
|
reject(error); |
|
}, |
|
}); |
|
}); |
|
}; |
|
|
|
const toCurrencyString = (value, currency) => { |
|
const locale = navigator.languages ?? "en-US"; |
|
|
|
try { |
|
return value.toLocaleString(locale, { |
|
style: "currency", |
|
currency: currency, |
|
}); |
|
} catch (e) { |
|
return `${value.toLocaleString(locale)} ${currency.toUpperCase()}`; |
|
} |
|
}; |
|
|
|
const updateRates = async () => { |
|
const url = `https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/jpy.min.json?${Math.random()}`; |
|
const data = await makeRequest(url); |
|
const rates = JSON.parse(data); |
|
|
|
// 12-hour timeout for the value |
|
GM_setValue("timeout", Date.now() + 12 * 3600 * 1000); |
|
GM_setValue("rates", rates); |
|
|
|
print_debug("updateCurrency", rates); |
|
return rates; |
|
}; |
|
|
|
const getRates = async () => { |
|
if (currency) return; |
|
|
|
const timeout = GM_getValue("timeout", null); |
|
const cachedRates = GM_getValue("rates", null); |
|
const rateDate = cachedRates ? Date.parse(cachedRates.date) : Date.now(); |
|
print_debug("getRates CACHE", timeout, cachedRates); |
|
|
|
// No cache OR no timeout OR timeout is after the current date OR rate internal date is after 2 days from now (failsafe) |
|
if ( |
|
!cachedRates || |
|
!timeout || |
|
timeout <= Date.now() || |
|
rateDate + 48 * 3600 * 1000 <= Date.now() |
|
) { |
|
currency = await updateRates(); |
|
print_debug("getRates NEW", currency); |
|
} else { |
|
currency = cachedRates; |
|
print_debug("getRates CACHED", currency); |
|
} |
|
}; |
|
|
|
const getCreatorName = (url) => { |
|
return url.split("/")[3].replace("@", ""); |
|
}; |
|
|
|
const fetchCreatorData = async (creatorName) => { |
|
const url = `https://skeb.jp/api/users/${creatorName}`; |
|
|
|
if (creatorCache.has(creatorName)) { |
|
const data = creatorCache.get(creatorName); |
|
print_debug("Using cached creator data:", data); |
|
extractData(url, data); |
|
return creatorCache.get(creatorName); |
|
} |
|
|
|
const response = await makeRequest(url, getBearerToken()); |
|
const data = JSON.parse(response); |
|
creatorCache.set(creatorName, data); |
|
extractData(url, data); |
|
print_debug("Fetched new creator data:", data); |
|
print_debug("Creator cache size:", creatorCache.size); |
|
return data; |
|
}; |
|
|
|
const toBlur = "img, .is-thumbnail, .plyr__video-wrapper"; |
|
const blurNsfwWorks = () => { |
|
if (!settings.blurNsfw) return; |
|
|
|
const elements = document.querySelectorAll(".card-wrapper:not(.done)"); |
|
if (!elements.length) return; |
|
print_debug(`Looking for something to blur in ${elements.length} cards...`); |
|
|
|
elements.forEach((work) => { |
|
work.classList.add("done"); |
|
const id = workPage(work.href); |
|
|
|
if (!worksCache.has(id)) return; |
|
work.querySelector(toBlur)?.classList.add("nsfw-blur"); |
|
}); |
|
}; |
|
|
|
const findElements = async (node) => { |
|
// 0. All cards |
|
blurNsfwWorks(); |
|
|
|
currentPageUrl = window.location.href; |
|
|
|
if (!node) { |
|
print_debug("-- Node is empty"); |
|
node = document.querySelector("section.section"); |
|
} |
|
|
|
if (node === null) { |
|
print_debug("-- No nodes"); |
|
return; |
|
} |
|
|
|
let pricesToUpdate = []; |
|
let timestampsToUpdate = []; |
|
|
|
// 1. Profile page, left column |
|
if ( |
|
currentPageUrl.includes("skeb.jp/@") && |
|
document.querySelector(".cover-image") |
|
) { |
|
print_debug("-- Profile page, left column"); |
|
|
|
// Recommended amount |
|
const elements = node.querySelectorAll("small:not(.done)"); |
|
// Cannot use `forEach` because we need i+1 element |
|
for (let i = 0; i < elements.length; i++) { |
|
if (elements[i].innerText === "Recommended amount") |
|
pricesToUpdate.push([elements[i + 1], true]); |
|
} |
|
} |
|
|
|
// 2. Requests |
|
if ( |
|
currentPageUrl.includes("skeb.jp/requests") || |
|
currentPageUrl.includes("skeb.jp/appeals/") |
|
) { |
|
print_debug("-- Requests page"); |
|
|
|
// Price, timestamps |
|
node.querySelectorAll("span.tag:not(.done)").forEach((element) => { |
|
var text = element.innerText; |
|
|
|
// Price |
|
if (text.includes("JPY")) pricesToUpdate.push([element, false]); |
|
|
|
// Timestamps |
|
if (text.includes(" AM") || text.includes(" PM")) |
|
timestampsToUpdate.push([element]); |
|
}); |
|
} |
|
|
|
// 3. Charges |
|
if (currentPageUrl.includes("skeb.jp/charges")) { |
|
print_debug("-- Charges page"); |
|
|
|
// Timestamps |
|
node |
|
.querySelectorAll("tbody > tr td:nth-child(3):not(.done)") |
|
.forEach((element) => { |
|
if (element.innerText.includes("/")) |
|
timestampsToUpdate.push([element, false]); |
|
}); |
|
|
|
// Price |
|
node |
|
.querySelectorAll("tbody > tr td:nth-child(6):not(.done)") |
|
.forEach((element) => { |
|
if (element.innerText.includes("JPY")) |
|
pricesToUpdate.push([element, true]); |
|
}); |
|
} |
|
|
|
// 4. Work page |
|
if (currentPageUrl.includes("/works/")) { |
|
print_debug("-- Works page"); |
|
|
|
if (document.querySelector("table#miniProfile")) return; |
|
|
|
const creatorName = getCreatorName(currentPageUrl); |
|
await fetchCreatorData(creatorName); |
|
injectCreatorData(); |
|
} |
|
|
|
// 5. Order page |
|
if (currentPageUrl.endsWith("/order")) { |
|
print_debug("-- Order page"); |
|
|
|
const amountInput = document.querySelector( |
|
"#new-request-panel > div:nth-child(5) > div.field-body > div > div.control > div > div.control.is-flexible > input:not(.done)" |
|
); |
|
const amountQuestion = document.querySelector( |
|
"#new-request-panel > div:nth-child(5) > div.field-body > div > div.container.is-flex.is-justify-content-flex-end.pt-2 > div:not(.done)" |
|
); |
|
if (!amountInput) return; |
|
|
|
const amountTemplate = ` |
|
<div data-v-46662776 class="m-0 px-2 py-1 question-box is-size-7 has-background-white-ter" id="amountPlaceholder"> |
|
0.00 USD |
|
</div> |
|
`; |
|
amountQuestion.insertAdjacentHTML("afterbegin", amountTemplate); |
|
amountQuestion.classList.add("done"); |
|
|
|
amountInput.addEventListener("input", amountInputHandler); |
|
amountInput.classList.add("done"); |
|
amountInput.dispatchEvent(new Event("input")); |
|
} |
|
|
|
if (pricesToUpdate.length || timestampsToUpdate.length) |
|
print_debug("Elements to update:", pricesToUpdate, timestampsToUpdate); |
|
pricesToUpdate.forEach((x) => injectPrice(...x)); |
|
timestampsToUpdate.forEach((x) => injectTimestamp(...x)); |
|
}; |
|
|
|
const amountInputHandler = (event) => { |
|
const amountPlaceholder = document.querySelector("#amountPlaceholder"); |
|
if (!amountPlaceholder) return; |
|
|
|
const jpyPrice = event.target.value; |
|
const convertedPrice = convertPrice(jpyPrice, false); |
|
|
|
let separator = " / "; |
|
const finalText = Object.values(convertedPrice).map((value) => { |
|
return value.text; |
|
}); |
|
amountPlaceholder.innerText = finalText.join(separator); |
|
}; |
|
|
|
const injectPrice = async (element, full = true) => { |
|
if (!settings.conversionEnabled || !settings.conversionValutes.length) return; |
|
|
|
const jpyPrice = element.innerText |
|
.replace(/[^0-9.,-]+/g, "") |
|
.replace(".", "") |
|
.replace(",", ""); |
|
|
|
const convertedPrice = convertPrice(jpyPrice); |
|
|
|
const alerts = settings.priceAlerts; |
|
if (convertedPrice[alerts.valute]?.value >= alerts.danger) { |
|
element.style.color = "#FF0000"; |
|
} else if (convertedPrice[alerts.valute]?.value >= alerts.warning) { |
|
element.style.color = "#FFFF00"; |
|
} |
|
|
|
element.title = `Original price: ${element.innerText}`; |
|
|
|
let separator = " / "; |
|
if (full) separator = "<br/>"; |
|
|
|
const finalText = Object.values(convertedPrice).map((value) => { |
|
return value.text; |
|
}); |
|
element.innerHTML = finalText.join(separator); |
|
|
|
element.classList.add("done"); |
|
}; |
|
|
|
const convertPrice = (originalPrice, includeJpy = true) => { |
|
originalPrice = Number(originalPrice); |
|
|
|
let convertedPrices = {}; |
|
|
|
if (includeJpy) { |
|
convertedPrices.jpy = { |
|
value: originalPrice, |
|
text: toCurrencyString(originalPrice, "jpy"), |
|
}; |
|
} |
|
|
|
settings.conversionValutes.forEach((valute) => { |
|
const value = Math.floor( |
|
originalPrice * rates[valute.valute] * valute.correction |
|
); |
|
|
|
convertedPrices[valute.valute] = { |
|
value, |
|
text: toCurrencyString(value, valute.valute), |
|
}; |
|
}); |
|
|
|
print_debug("Converted prices", convertedPrices); |
|
return convertedPrices; |
|
}; |
|
|
|
const formatDays = (time) => { |
|
let text = "Unknown"; |
|
|
|
if (time) { |
|
const days = Math.floor(time / 60 / 60 / 24); |
|
if (days) { |
|
text = `${days} days`; |
|
} else { |
|
text = "< 1 day"; |
|
} |
|
} |
|
|
|
return text; |
|
}; |
|
|
|
const today = new Date(); |
|
const injectTimestamp = (element, withTime = true) => { |
|
const date = new Date(element.innerText); |
|
|
|
element.title = element.innerText; |
|
if (withTime) { |
|
element.innerText = date.toLocaleString(); |
|
} else { |
|
const diff = formatDays((today - date) / 1000); |
|
element.innerText = `${date.toLocaleDateString( |
|
navigator.languages ?? "en-US" |
|
)} (${diff})`; |
|
if (diff >= 150) element.style.color = "#FF0000"; |
|
} |
|
|
|
element.classList.add("done"); |
|
}; |
|
|
|
const injectCreatorData = () => { |
|
const creatorName = getCreatorName(window.location.href); |
|
if (!creatorCache.has(creatorName)) { |
|
print_debug("No creator data for", creatorName); |
|
return; |
|
} |
|
|
|
if (document.querySelector("table#miniProfile")) return; |
|
|
|
const element = document.querySelector("section.section div.is-divider"); |
|
const table = ` |
|
<table class="table is-fullwidth is-narrow" id="miniProfile"> |
|
<tbody> |
|
${miniProfile(creatorName)} |
|
</tbody> |
|
</table> |
|
<div class="is-divider"></div> |
|
`; |
|
|
|
element.insertAdjacentHTML("afterEnd", table); |
|
}; |
|
|
|
const skillGenre = { |
|
art: "Artwork", |
|
comic: "Comic", |
|
voice: "Voice", |
|
novel: "Text", |
|
video: "Video", |
|
music: "Music", |
|
correction: "Advice", |
|
}; |
|
|
|
const miniProfile = (creatorName) => { |
|
let template = ""; |
|
|
|
const creatorData = creatorCache.get(creatorName); |
|
if (!creatorData.acceptable) { |
|
template += ` |
|
<tr> |
|
<td><small style="color: #FF0000">Not seeking</small></td> |
|
<td></td> |
|
</tr> |
|
`; |
|
} |
|
|
|
creatorData.skills.forEach((skill) => { |
|
let type = skillGenre[skill.genre] ?? "Unknown"; |
|
|
|
if (!settings.conversionEnabled) { |
|
const amount = skill.default_amount.toLocaleString( |
|
navigator.languages ?? "en-US", |
|
{ |
|
style: "currency", |
|
currency: "jpy", |
|
} |
|
); |
|
|
|
template += ` |
|
<tr> |
|
<td><small>${type}</small></td> |
|
<td><small>${amount}</small></td> |
|
</tr> |
|
`; |
|
} else { |
|
var convertedPrice = convertPrice(skill.default_amount); |
|
|
|
const alerts = settings.priceAlerts; |
|
let style; |
|
if (convertedPrice[alerts.valute]?.value >= alerts.danger) { |
|
style = "color: #FF0000"; |
|
} else if (convertedPrice[alerts.valute]?.value >= alerts.warning) { |
|
style = "color: #FFFF00"; |
|
} |
|
|
|
const finalText = Object.values(convertedPrice).map((value) => { |
|
return value.text; |
|
}); |
|
|
|
template += ` |
|
<tr title="Original price: ${convertedPrice.jpy.text}"> |
|
<td><small>${type}</small></td> |
|
<td><small style="${style}">${finalText.join("<br/>")}</small></td> |
|
</tr> |
|
`; |
|
} |
|
}); |
|
|
|
template += daysTemplate( |
|
creatorData.received_requests_average_response_time, |
|
"Response average" |
|
); |
|
template += daysTemplate( |
|
creatorData.completing_average_time, |
|
"Complete average" |
|
); |
|
|
|
template += ` |
|
<tr> |
|
<td><small>Total</small></td> |
|
<td><small>${creatorData.received_works_count}</small></td> |
|
</tr> |
|
`; |
|
|
|
template += ` |
|
<tr> |
|
<td><small>Complete rate</small></td> |
|
<td><small style="${ |
|
creatorData.complete_rate < 0.95 ? "color: #ff0000" : "" |
|
}"> |
|
${creatorData.complete_rate * 100}% |
|
</small></td> |
|
</tr> |
|
`; |
|
|
|
template += ` |
|
<tr> |
|
<td><small>Has NSFW works?</small></td> |
|
<td> |
|
<small style="${creatorData.nsfw_acceptable ? "color: #ff0000" : ""}"> |
|
${creatorData.nsfw_acceptable ? "Yes" : "No"} (${ |
|
creatorData.received_nsfw_works_count |
|
}) |
|
</small> |
|
</td> |
|
</tr> |
|
`; |
|
|
|
return template; |
|
}; |
|
|
|
const workPage = (url) => { |
|
return url |
|
.replace("https://skeb.jp", "") |
|
.replace("/@", "") |
|
.replace("/works/", ":"); |
|
}; |
|
|
|
const daysTemplate = (time, text) => { |
|
const days = formatDays(time); |
|
const daysNumber = Number( |
|
days.replace("< ", "").replace(" day", "").replace("s", "") |
|
); |
|
|
|
let style = ""; |
|
if (time === undefined || daysNumber === NaN || daysNumber > 60) { |
|
style = "color: #FF0000"; |
|
} |
|
|
|
return ` |
|
<tr> |
|
<td><small>${text}</small></td> |
|
<td><small style="${style}">${days}</small></td> |
|
</tr> |
|
`; |
|
}; |
|
|
|
const getBearerToken = () => { |
|
return { |
|
Authorization: `Bearer ${window.localStorage.token}`, |
|
}; |
|
}; |
|
|
|
const setupUserCardHoverObserver = async () => { |
|
document.body?.addEventListener("mouseover", async (event) => { |
|
let link = null; |
|
|
|
// console.log(event.target); |
|
if (event.target.matches("div.card")) { |
|
print_debug("Card hover:", event.target.parentElement); |
|
link = event.target.parentElement?.href; |
|
} else if (event.target.matches("div.title.is-5")) { |
|
print_debug("User link hover:", event.target); |
|
link = event.target.parentElement?.parentElement?.href; |
|
} else if (event.target.matches("div.level-item.is-wrap.is-block")) { |
|
print_debug("User link hover, higher:", event.target); |
|
link = event.target.parentElement?.href; |
|
} |
|
|
|
if (!link) return; |
|
print_debug("Links to fetch", link); |
|
|
|
const creatorName = getCreatorName(link); |
|
await fetchCreatorData(creatorName); |
|
}); |
|
}; |
|
|
|
const intercept = async (data) => { |
|
if (!data?.responseURL) return; |
|
const url = data.responseURL; |
|
|
|
try { |
|
const response = JSON.parse(data.response); |
|
|
|
if (url === `${apiEndpoint}/account`) { |
|
currentAccount = response.screen_name; |
|
print_debug("currentAccount is", currentAccount); |
|
return; |
|
} |
|
|
|
extractData(url, response); |
|
} catch (e) { |
|
print_debug("Not a JSON:", data); |
|
} |
|
}; |
|
|
|
const extractData = (url, response) => { |
|
let works = []; |
|
|
|
try { |
|
if (url.includes("/friend_works?") || url.includes("/works?")) { |
|
works.push(...response); |
|
} else if (url.includes("/followings")) { |
|
works.push(...response.following_works, ...response.friend_works); |
|
} else if (url.includes("/users/")) { |
|
if ("similar_works" in response) works.push(...response.similar_works); |
|
if ("sent_works" in response) works.push(...response.sent_works); |
|
if ("received_works" in response) works.push(...response.received_works); |
|
} else if (url === apiEndpoint) { |
|
works.push( |
|
...response.new_art_works, |
|
...response.new_comic_works, |
|
...response.new_correction_works, |
|
...response.new_music_works, |
|
...response.new_novel_works, |
|
...response.new_video_works, |
|
...response.new_voice_works, |
|
...response.popular_works |
|
); |
|
} else if (url.includes("/indexes/*/queries?")) { |
|
response.results.forEach((result) => { |
|
if (result.index !== "Request") return; |
|
works.push(...result.hits); |
|
}); |
|
} |
|
|
|
works.forEach((work) => { |
|
if (work.nsfw || work.hardcore) worksCache.add(workPage(work.path)); |
|
}); |
|
} catch (e) { |
|
print_debug(e); |
|
} |
|
|
|
window.localStorage.setItem("helper-works", JSON.stringify([...worksCache])); |
|
print_debug("Updated the works cache", url, worksCache); |
|
}; |
|
|
|
let settingsLinkDone = false; |
|
const insertSettingsLink = () => { |
|
if (settingsLinkDone) return; |
|
|
|
const dropdown = document.querySelector( |
|
".navbar-menu .navbar-dropdown.is-right" |
|
); |
|
if (!dropdown) { |
|
return; |
|
} |
|
|
|
const settingsLink = `<a href="#" data-v-3c8a55f2 class="navbar-item" id="settings-link">Skeb Helper Settings</a><div data-v-3c8a55f2="" class="navbar-divider"></div>`; |
|
// settingsLink. |
|
dropdown.insertAdjacentHTML("afterbegin", settingsLink); |
|
document |
|
.getElementById("settings-link") |
|
.addEventListener("click", (event) => { |
|
gms.open(); |
|
}); |
|
|
|
settingsLinkDone = true; |
|
}; |
|
|
|
const initSettingsModal = () => { |
|
document.body.appendChild(frame); |
|
|
|
// Open settings pane if this is the first time |
|
if (GM_getValue("firstTime") !== true) { |
|
gms.open(); |
|
GM_setValue("firstTime", true); |
|
} |
|
|
|
insertSettingsLink(); |
|
|
|
print_debug("Current settings:", settings); |
|
}; |
|
|
|
const main = async () => { |
|
print_debug("Main..."); |
|
|
|
// Init settings modal |
|
initSettingsModal(); |
|
|
|
// Setup card observer |
|
await setupUserCardHoverObserver(); |
|
|
|
// Injecting prices for the first time |
|
await findElements(); |
|
|
|
// Dynamically inject prices |
|
const observer = new MutationObserver(async (mutations, _observer) => { |
|
for (const mutation of mutations.filter((x) => x.addedNodes.length > 0)) { |
|
const node = mutation.addedNodes[0]; |
|
if (["LINK", "STYLE", "SCRIPT", "NOSCRIPT"].includes(node.tagName)) |
|
continue; |
|
if (node.tagName === undefined) continue; |
|
|
|
if (node.querySelector(".navbar-dropdown")) insertSettingsLink(); |
|
|
|
await findElements(node); |
|
} |
|
}); |
|
|
|
observer.observe(document.body, { |
|
childList: true, |
|
subtree: true, |
|
attributes: true, |
|
}); |
|
|
|
console.log("Skeb Helper: main() finished"); |
|
}; |
|
|
|
// Add styles |
|
GM_addStyle(` |
|
.nsfw-blur { |
|
filter: blur(10px); |
|
} |
|
|
|
.nsfw-blur:hover { |
|
filter: blur(0); |
|
transition: filter 0.1s; |
|
} |
|
`); |
|
|
|
// Get works from the storage (cold start) |
|
const worksStorage = |
|
JSON.parse(window.localStorage.getItem("helper-works")) ?? []; |
|
print_debug("Works grabbed from local storage:", worksStorage); |
|
worksCache = new Set(worksStorage.slice(1, 1000)); |
|
|
|
// Send request to /account for the homepage |
|
await makeRequest(`${apiEndpoint}/account`, getBearerToken()); |
|
|
|
// Send request to /api for the homepage |
|
const apiResponse = await makeRequest(apiEndpoint, getBearerToken()); |
|
extractData(apiEndpoint, JSON.parse(apiResponse)); |
|
|
|
// Updating currency |
|
await getRates(); |
|
print_debug("Current rates", currency); |
|
|
|
// Grabbing the rate |
|
settings.conversionValutes.map((valute) => { |
|
rates[valute.valute] = currency.jpy[valute.valute]; |
|
}); |
|
print_debug("Effective rate", rates); |
|
|
|
console.log("Skeb Helper: Initiated"); |
|
|
|
window.onload = main(); |