Web-Ace Chapter Downloader

Download chapters from Web-Ace

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Web-Ace Chapter Downloader
// @name:ja      Web-Ace チャプターダウンローダー
// @namespace    https://ceavan.com/
// @version      1.3
// @description  Download chapters from Web-Ace
// @description:ja  Web-Aceから章をダウンロード
// @author       ceavan
// @icon         https://appfav.net/image/thum_icon1/20200311105141474179OW21flGbBw.jpg
// @license MIT
// @match        https://web-ace.jp/*/episode/*/
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==

(function() {
    'use strict';

    // Load settings
    function loadSettings() {
        return {
            useCBZ: GM_getValue("useCBZ", false),
            includeChapterTitle: GM_getValue("includeChapterTitle", true),
        };
    }
    let { useCBZ, includeChapterTitle } = loadSettings();
    let menuIDs = [];

    // Function to save settings and refresh menu
    function updateSetting(key, value) {
        GM_setValue(key, value);
        ({ useCBZ, includeChapterTitle } = loadSettings());
        registerMenuCommands();
    }

    // Function to create and show a progress bar
    function createProgressBar() {
        let progressContainer = document.createElement("div");
        progressContainer.id = "progress-container";
        progressContainer.style.position = "fixed";
        progressContainer.style.bottom = "80px";
        progressContainer.style.right = "10px";
        progressContainer.style.width = "300px";
        progressContainer.style.height = "40px";
        progressContainer.style.border = "1px solid #000";
        progressContainer.style.background = "#ddd";
        progressContainer.style.zIndex = 1000;
        progressContainer.style.display = "flex";
        progressContainer.style.alignItems = "center";
        progressContainer.style.justifyContent = "center";
        progressContainer.style.fontSize = "20px";
        progressContainer.style.fontWeight = "bold";
        progressContainer.style.color = "#000";
        progressContainer.style.overflow = "hidden";
        progressContainer.style.textAlign = "center";

        let progressBar = document.createElement("div");
        progressBar.id = "progress-bar";
        progressBar.style.height = "100%";
        progressBar.style.width = "0%";
        progressBar.style.background = "#4caf50";
        progressBar.style.position = "absolute";
        progressBar.style.top = "0";
        progressBar.style.left = "0";
        progressBar.style.transition = "width 0.3s ease";

        let progressText = document.createElement("div");
        progressText.id = "progress-text";
        progressText.style.position = "absolute";
        progressText.style.width = "100%";
        progressText.style.textAlign = "center";
        progressText.style.fontSize = "20px";
        progressText.style.fontWeight = "bold";
        progressText.style.zIndex = "10";
        progressText.style.whiteSpace = "pre-line";

        progressContainer.appendChild(progressBar);
        progressContainer.appendChild(progressText);
        document.body.appendChild(progressContainer);
    }

    // Function to update the progress bar
    function updateProgress(current, total) {
        let progressBar = document.getElementById("progress-bar");
        let progressText = document.getElementById("progress-text");
        if (progressBar && progressText) {
            let percent = Math.round((current / total) * 100);
            progressBar.style.width = percent + "%";
            progressText.innerText = `${current} / ${total}`;
        }
    }

    // Function to update the final progress text when ZIP is being saved
    function updateSavingText(total) {
        let progressText = document.getElementById("progress-text");
        if (progressText) {
            progressText.innerText = `Saving ZIP...`;
        }
    }

    // Function to update the final status after ZIP is saved
    function updateCompletedText() {
        let progressText = document.getElementById("progress-text");
        let downloadButton = document.getElementById("download-chapter-button");

        if (progressText) {
            progressText.innerText = "Chapter downloaded";
        }

        if (downloadButton) {
            setTimeout(() => {
                downloadButton.innerText = "Chapter Downloaded";
            }, 2000); // Reset button after 2 seconds
        }

        setTimeout(() => {
            let progressContainer = document.getElementById("progress-container");
            if (progressContainer) progressContainer.remove();
        }, 2000); // Remove after 2 seconds
    }

    // Function to extract the chapter title from <p class="watitle">
    function getChapterTitle() {
        let titleElement = document.querySelector("p.watitle");
        return titleElement ? titleElement.innerText.trim() : "";
    }

    // Function to download images and pack them into a zip file
    function downloadImages(imageUrls) {
        const zip = new JSZip();
        let totalImages = imageUrls.length;
        let padLength = totalImages.toString().length;
        let downloadCount = 0;

        createProgressBar();

        imageUrls.forEach((url, index) => {
            let fileNumber = String(index + 1).padStart(padLength, '0');
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                responseType: "blob",
                onload: function(response) {
                    zip.file(`${fileNumber}.jpg`, response.response, { binary: true });
                    downloadCount++;
                    updateProgress(downloadCount, totalImages);

                    if (downloadCount === totalImages) {
                        updateSavingText(totalImages);
                        let title = document.title.split('|')[0].trim();
                        let chapterTitle = includeChapterTitle ? getChapterTitle() : "";

                        // Append chapter title if it exists
                        let finalFileName = chapterTitle ? `${title} - ${chapterTitle}` : title;

                        // Determine the extension based on CBZ or ZIP preference
                        let fileExtension = useCBZ ? "cbz" : "zip";
                        finalFileName = `${finalFileName}.${fileExtension}`;

                        zip.generateAsync({ type: "blob" }).then(function(content) {
                            saveAs(content, finalFileName);
                            updateCompletedText();
                        });
                    }
                },
                onerror: function(error) {
                    console.error('Failed to download image:', error);
                }
            });
        });
    }

    // Function to get image URLs from JSON endpoint
    function getImageUrls(jsonUrl) {
        GM_xmlhttpRequest({
            method: "GET",
            url: jsonUrl,
            onload: function(response) {
                const imageUrls = JSON.parse(response.responseText);
                downloadImages(imageUrls);
            },
            onerror: function(error) {
                console.error('Failed to fetch image URLs:', error);
            }
        });
    }

    // Add download button to the page
    const button = document.createElement("button");
    button.id = "download-chapter-button";
    button.innerHTML = "Download Chapter";
    button.style.position = "fixed";
    button.style.bottom = "40px";
    button.style.right = "50px";
    button.style.fontSize= "20px";
    button.style.zIndex = 1000;
    button.onclick = function() {
        button.innerText = "Downloading chapter...";
        button.disabled = true;

        const chapterUrl = window.location.href;
        const jsonUrl = chapterUrl + 'json/';
        getImageUrls(jsonUrl);
    };
    document.body.appendChild(button);

    // Function to register or refresh menu commands
    function registerMenuCommands() {
        // Unregister old menu commands
        menuIDs.forEach(id => GM_unregisterMenuCommand(id));
        menuIDs = []; // Reset stored command IDs

        // Register new menu commands
        menuIDs.push(GM_registerMenuCommand(
            `${useCBZ ? "✅" : "❌"} Save as CBZ`,
            () => {
                updateSetting("useCBZ", !useCBZ);
            }
        ));

        menuIDs.push(GM_registerMenuCommand(
            `${includeChapterTitle ? "✅" : "❌"} Include chapter title in filename`,
            () => {
                updateSetting("includeChapterTitle", !includeChapterTitle);
            }
        ));
    }

    // Register menu commands on script load
    registerMenuCommands();
})();