Fav4Pawchive

Consistent Favorite Button for Pawchive Creator Pages

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
})();