Sleazy Fork is available in English.
Add a ♥ favorite button to each thumbnail; detects existing favorites via staggered fetches
// ==UserScript==
// @name Motherless Quick Favorite
// @namespace https://motherless.com/
// @version 1.2
// @description Add a ♥ favorite button to each thumbnail; detects existing favorites via staggered fetches
// @author You
// @match https://motherless.com/*
// @grant GM_xmlhttpRequest
// @connect motherless.com
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Delay between each status-check fetch (ms). Increase if you get rate-limited.
const FETCH_DELAY_MS = 400;
const BUTTON_STYLE = `
position: absolute;
top: 4px;
left: 4px;
z-index: 999;
background: rgba(0,0,0,0.65);
color: #fff;
border: none;
border-radius: 5px;
padding: 5px 9px;
font-size: 20px;
cursor: pointer;
line-height: 1;
transition: background 0.15s;
`;
function getMediaId(thumbEl) {
const inner = thumbEl.querySelector('[data-codename]');
if (inner) return inner.getAttribute('data-codename');
const link = thumbEl.querySelector('a[href]');
if (!link) return null;
const match = link.getAttribute('href').match(/\/([A-Z0-9]+)$/i);
return match ? match[1] : null;
}
function setButtonState(btn, state) {
if (state === 'favorited') {
btn.textContent = '♥';
btn.style.background = 'rgba(180,30,30,0.85)';
btn.disabled = true;
btn.title = 'Already favorited';
} else if (state === 'loading') {
btn.textContent = '…';
btn.style.background = 'rgba(0,0,0,0.65)';
btn.disabled = true;
} else if (state === 'done') {
btn.textContent = '♥';
btn.style.background = 'rgba(180,30,30,0.85)';
btn.disabled = true;
btn.title = 'Favorited!';
} else if (state === 'error') {
btn.textContent = '✕';
btn.style.background = 'rgba(180,0,0,0.85)';
btn.disabled = false;
btn.title = 'Error – click to retry';
}
}
function checkFavoriteStatus(mediaId, btn) {
GM_xmlhttpRequest({
method: 'GET',
url: `https://motherless.com/${mediaId}`,
headers: { 'Referer': window.location.href },
onload: function (res) {
if (res.status === 200) {
// If the "remove from favorites" button is present, it's already favorited
if (res.responseText.indexOf('button-favorites-remove-upload') !== -1) {
setButtonState(btn, 'favorited');
}
// else leave as default ♡
}
},
});
}
function favoriteMedia(mediaId, btn) {
setButtonState(btn, 'loading');
GM_xmlhttpRequest({
method: 'POST',
url: `https://motherless.com/favorites/add?codename=${mediaId}`,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': window.location.href,
},
onload: function (res) {
if (res.status === 200) {
setButtonState(btn, 'done');
} else {
console.warn('[QuickFav] Unexpected status:', res.status);
setButtonState(btn, 'error');
}
},
onerror: function () {
setButtonState(btn, 'error');
},
});
}
// Queue of {mediaId, btn} pending a status check
const checkQueue = [];
let queueRunning = false;
function enqueueCheck(mediaId, btn) {
checkQueue.push({ mediaId, btn });
if (!queueRunning) runQueue();
}
function runQueue() {
if (checkQueue.length === 0) {
queueRunning = false;
return;
}
queueRunning = true;
const { mediaId, btn } = checkQueue.shift();
checkFavoriteStatus(mediaId, btn);
setTimeout(runQueue, FETCH_DELAY_MS);
}
function injectButtons() {
const thumbs = document.querySelectorAll('.thumb-container:not([data-qfav])');
thumbs.forEach(function (thumb) {
thumb.setAttribute('data-qfav', '1');
const mediaId = getMediaId(thumb);
if (!mediaId) return;
const pos = window.getComputedStyle(thumb).position;
if (pos === 'static') thumb.style.position = 'relative';
const btn = document.createElement('button');
btn.textContent = '♡';
btn.title = 'Quick Favorite';
btn.setAttribute('style', BUTTON_STYLE);
btn.addEventListener('mouseenter', () => {
if (!btn.disabled) btn.style.background = 'rgba(200,40,40,0.9)';
});
btn.addEventListener('mouseleave', () => {
if (!btn.disabled) btn.style.background = 'rgba(0,0,0,0.65)';
});
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
favoriteMedia(mediaId, btn);
});
thumb.appendChild(btn);
// Queue a status check with staggered delay
enqueueCheck(mediaId, btn);
});
}
injectButtons();
const observer = new MutationObserver(function () {
injectButtons();
});
observer.observe(document.body, { childList: true, subtree: true });
})();