MangaLib helper

Измененние UX сайта для удобного чтения и навигации

目前為 2024-06-19 提交的版本,檢視 最新版本

// ==UserScript==
// @name           MangaLib helper
// @version        0.0.2
// @description    Измененние UX сайта для удобного чтения и навигации
// @icon           https://mangalib.me/icons/android-icon-192x192.png?333
// @match          https://mangalib.me
// @grant          unsafeWindow
// @grant          GM.xmlHttpRequest
// @run-at         document-end
// @require        https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.js
// @require        https://cdnjs.cloudflare.com/ajax/libs/lscache/1.3.2/lscache.min.js
// @namespace https://greasyfork.org/users/728771
// ==/UserScript==

/* jshint asi: true, esnext: true, -W097 */
(async function ($) {
    'use strict'
    lscache.flushExpired
    unsafeWindow.$ = $;
    unsafeWindow.GM_xmlhttpRequest = GM.xmlHttpRequest;

    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) return resolve(document.querySelector(selector));

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    const serviceSymbol = Symbol('serviceSymbol');
    const funcs = {
        [serviceSymbol]: {
            senkuroDetails: async (slug) => {
                return new Promise(resolve => {
                    return GM_xmlhttpRequest({
                        method: "POST",
                        url: "https://api.senkuro.com/graphql",
                        data: `{"extensions":{"persistedQuery":{"sha256Hash":"a44132a9483c73f8db43edf8d171c8e108de93ad3c990148f8474ffc546901e9","version":1}},"operationName":"fetchManga","variables":{"slug": "${slug}"}}`,
                        onload: function (response) {
                            const data = JSON.parse(response.responseText);
                            if (!data?.data?.manga) resolve();
                            if (data.data.manga?.branches.length) resolve(Math.max(...data.data.manga.branches.map(branch => branch.chapters)))
                            resolve(data.data.manga.chapters);
                        }
                    });
                })
            },
            readmangaDetails: async (url) => {
                return new Promise(resolve => {
                    return GM_xmlhttpRequest({
                        method: "GET",
                        url,
                        onload: async response => {
                            resolve($(response.responseText).find('.chapters tr a').eq(0)[0]?.firstChild.nodeValue.trim());
                        }
                    });
                })
            }
        },
        Senkuro: async (slug, titles) => {
            if (lscache.get(`${slug}--senkuro`)) return lscache.get(`${slug}--senkuro`)
            return new Promise(resolve => {
                return GM_xmlhttpRequest({
                    method: "POST",
                    url: "https://api.senkuro.com/graphql",
                    data: `{"extensions":{"persistedQuery":{"sha256Hash":"a2ccb7472c4652e21a7914940cce335683d37abdecccfeb6bbf9674e0a5bda80","version":1}},"operationName":"search","variables":{"query":"${titles[0]}","type":"MANGA"}}`,
                    onload: async response => {
                        const data = JSON.parse(response.responseText);
                        const slug = data.data.search.edges[0]?.node.slug;
                        if (!slug) return;
                        const chapter = await funcs[serviceSymbol].senkuroDetails(slug);
                        if (!chapter) return;
                        lscache.set(`${slug}--senkuro`, [`https://senkuro.com/manga/${slug}/chapters`, chapter], 3600000)
                        resolve([`https://senkuro.com/manga/${slug}/chapters`, chapter])
                    }
                });
            })
        },
        Readmanga: async (slug, titles) => {
            if (lscache.get(`${slug}--readmanga`)) return lscache.get(`${slug}--readmanga`)
            return new Promise(resolve => {
                return GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://readmanga.live/search/suggestion?query=${$('.media-name__main').text()}`,
                    onload: async response => {
                        const data = JSON.parse(response.responseText);
                        const result = data.suggestions.find(suggestion => titles.filter(value => [suggestion.value, ...suggestion.names].includes(value)).length)
                        if (!result) return;
                        const slug = result.link;
                        const chapter = await funcs[serviceSymbol].readmangaDetails(`https://readmanga.live${slug}`);
                        if (!chapter) return;
                        lscache.set(`${slug}--readmanga`, [`https://readmanga.live${slug}#chapters-list`, chapter], 3600000)
                        resolve([`https://readmanga.live${slug}#chapters-list`, chapter])
                    }
                });
            })
        }
    }

    // language=HTML
    const dropdownDOM = `
        <div class='dropdown button_block'>
            <button class='dropbtn button button_primary button_block'>
                Открыть на сайте
                <i class='fa fa-caret-down'></i>
            </button>
            <div class='dropdown-content media-info-list paper'></div>
        </div>
    `;

    async function mangaPage() {
        await waitForElm('.media-name__main');

        const slug = new URL(unsafeWindow.location).pathname.match(/[^\/]+/g)
        const titles = [
            $('.media-name__main').text().trim(),
            $('.media-name__alt').text().trim(),
            ...$('.media-info-list__item_alt-names .media-info-list__value div').toArray().map(function (i) {
                return i.innerText
            })
        ]

        const teamsWrapper = $('.media-chapters-teams')
        const tab = $('.tabs__item[data-key="chapters"]');
        let lastChapter = 0;
        let team;

        if (teamsWrapper.length) {
            const teams = teamsWrapper.find('.team-list-item');
            for (const node of teams.toArray()) {
                const vue = node.__vue__;
                const propsData = vue.$options.propsData;
                const isSubscribed = propsData.branch.is_subscribed;
                const lastTeamChapter = propsData.lastCreatedChapters[propsData.branch.id].chapter_number;
                if (isSubscribed || lastChapter < lastTeamChapter) {
                    lastChapter = lastTeamChapter;
                    team = node;
                    if (isSubscribed) break;
                }
            }
        } else lastChapter = $('.media-chapters .media-chapters-list .media-chapter')[0].__vue__.$options.propsData.chapter.chapter_number

        const state = $('.media-sidebar button').text().trim();
        let url = '';
        switch (state) {
            case 'Читаю':
                tab.click();
                $('.media-sidebar button').after(dropdownDOM);
                $(".dropdown-content").hide();
                $(".dropbtn").click(() => $(".dropdown-content").toggle());
                for (const website in funcs) {
                    const url = await funcs[website](slug, titles);
                    if (url) $('.dropdown-content').append(`<a href="${url[0]}" class="media-info-list__item">${website} | ${url[1]}</a>`)
                }
                $(".dropdown-content a").click(() => $(".dropdown-content").hide());
                break;
            case 'Senkuro':
            case 'Readmanga':
                tab.click();
                url = await funcs[state](slug, titles);
                if (url) $('.media-sidebar button').after(`<a href="${url[0]}" class="button button_block button_primary">Продолжить на ${state} | ${url[1]}</a>`);
        }

        tab.text(`${tab.text()} | ${lastChapter}`);
        setTimeout(() => $(team).click(), 100)
    }

    async function chapterPage() {
        $(window).scroll(function () {
            const scrolledTo = window.scrollY + window.innerHeight;
            const isReachBottom = document.body.scrollHeight === scrolledTo;
            if (isReachBottom) {
                $('.reader-bookmark').not(".is-marked").click();
                $('.reader-next__btn.button_label_right')[0]?.click();
            }
        });
    }

    $(document).ready(() => {
        const path = new URL(unsafeWindow.location).pathname.replace(/^\//, '');
        if (!path.includes('/')) mangaPage();
        else if (path.match(/v\d+\/c\d+/)) chapterPage();
    });
}).bind(this)(jQuery)

jQuery.noConflict()