Consistent Favorite Button for Pawchive Creator Pages
// ==UserScript==
// @name Fav4Pawchive
// @version 1
// @match *://pawchive.pw/*
// @grant GM_addStyle
// @run-at document-end
// @description Consistent Favorite Button for Pawchive Creator Pages
// @namespace c0032
// @license ohforfuckssakeidontcare
// ==/UserScript==
(function () {
'use strict';
GM_addStyle(`
.restored-fav-btn {
display: none !important;
align-items: center;
gap: 3px;
padding: 0;
margin: 0;
background: #ff4081;
color: white;
border: none;
border-radius: 3px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
z-index: 99999;
}
.restored-fav-btn.visible { display: inline-flex !important; }
.restored-fav-btn:hover { background: #f50057; transform: translateX(-50%) scale(1.05); }
.restored-fav-btn.faved { background: #4caf50; }
.restored-fav-btn.faved:hover { background: #388e3c; }
`);
const state = {
service: null,
id: null,
isFaved: false,
isFetching: false
};
const btn = document.createElement('button');
btn.className = 'restored-fav-btn';
document.body.appendChild(btn);
btn.addEventListener('click', async () => {
if (!state.service || !state.id || state.isFetching) return;
const method = state.isFaved ? 'DELETE' : 'POST';
const url = `/api/v1/favorites/creator/${state.service}/${state.id}`;
state.isFetching = true;
try {
const res = await fetch(url, {
method,
credentials: 'include',
headers: { 'Content-Type': 'application/json' }
});
if (res.ok || res.status === 204) {
state.isFaved = !state.isFaved;
render();
} else {
alert(`Failed (${res.status}). Are you logged in?`);
}
} catch (e) {
console.error('Toggle failed', e);
} finally {
state.isFetching = false;
}
});
function render() {
if (!state.service || !state.id) {
btn.classList.remove('visible');
return;
}
btn.classList.add('visible');
btn.textContent = state.isFaved ? '★ Unfavorite' : '☆ Favorite';
btn.classList.toggle('faved', state.isFaved);
}
async function checkStatus() {
if (!state.service || !state.id) return;
const currentKey = `${state.service}/${state.id}`;
state.isFetching = true;
try {
const res = await fetch(`/api/v1/account/favorites?type=artist`, { credentials: 'include' });
if (!res.ok) throw new Error();
const data = await res.json();
if (currentKey !== `${state.service}/${state.id}`) return;
state.isFaved = data.some(item =>
item.service === state.service &&
(String(item.id) === state.id || String(item.relation_id) === state.id)
);
render();
} catch (e) {
console.warn('Check status failed', e);
} finally {
state.isFetching = false;
}
}
function handleNavigation() {
const match = window.location.pathname.match(/^\/([^/]+)\/user\/([^/]+)/);
const newService = match ? match[1] : null;
const newId = match ? match[2] : null;
if (state.service !== newService || state.id !== newId) {
state.service = newService;
state.id = newId;
state.isFaved = false;
render();
checkStatus();
} else if (!document.body.contains(btn)) {
document.body.appendChild(btn);
}
}
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
handleNavigation();
};
const originalReplaceState = history.replaceState;
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
handleNavigation();
};
window.addEventListener('popstate', handleNavigation);
handleNavigation();
})();