PixHost Enhanced

Drag-and-drop & Ctrl+V

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         PixHost Enhanced
// @namespace    https://pixhost.to/
// @version      0.2
// @description  Drag-and-drop & Ctrl+V
// @author       Colder
// @match        https://pixhost.to/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Add styles
    GM_addStyle(`
        #custom-upload-zone {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 300px;
            background: white;
            border: 2px solid #ccc;
            border-radius: 8px;
            padding: 15px;
            z-index: 9999;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        #drop-zone {
            border: 2px dashed #ccc;
            border-radius: 4px;
            padding: 20px;
            text-align: center;
            margin-bottom: 10px;
            background: #f9f9f9;
            transition: all 0.3s ease;
            cursor: pointer;
        }

        #drop-zone.drag-over {
            background: #e1f5fe;
            border-color: #2196F3;
        }

        .url-output {
            margin-top: 10px;
        }

        .url-textarea {
            width: 100%;
            min-height: 120px;
            margin: 5px 0;
            font-family: monospace;
            font-size: 12px;
            resize: vertical;
            white-space: pre;
        }

        .action-buttons {
            display: flex;
            gap: 5px;
            margin-top: 5px;
        }

        .copy-btn {
            background: #2196F3;
            color: white;
            border: none;
            padding: 8px 5px;
            border-radius: 4px;
            cursor: pointer;
            flex: 1;
            font-size: 11px;
            font-weight: bold;
            text-align: center;
            transition: background 0.2s;
        }

        .copy-btn:hover {
            background: #1976D2;
        }

        #status-container {
            margin-top: 10px;
            padding: 10px;
            border-radius: 4px;
            display: none;
            font-size: 13px;
            text-align: center;
        }

        .success {
            background: #E8F5E9;
            color: #2E7D32;
        }

        .error {
            background: #FFEBEE;
            color: #C62828;
        }

        .progress {
            margin-top: 10px;
            font-size: 0.9em;
            color: #666;
            text-align: center;
        }
    `);

    // Create upload interface
    const uploadInterface = document.createElement('div');
    uploadInterface.id = 'custom-upload-zone';
    uploadInterface.innerHTML = `
        <div id="drop-zone" title="Click to browse files">
            Drag & Drop Images Here<br>
            <small>or click / Ctrl+V to paste</small>
            <input type="file" id="file-input" multiple style="display: none" accept="image/*">
        </div>
        <div class="progress"></div>
        <div id="status-container"></div>
        <div class="url-output"></div>
    `;

    document.body.appendChild(uploadInterface);

    // Setup elements
    const dropZone = document.getElementById('drop-zone');
    const fileInput = document.getElementById('file-input');
    const urlOutput = document.querySelector('.url-output');
    const progressDiv = document.querySelector('.progress');
    const statusContainer = document.getElementById('status-container');

    let uploadQueue = [];
    let uploadResults = [];
    let isUploading = false;
    let statusTimeout;

    // Event Listeners
    dropZone.addEventListener('click', () => fileInput.click());

    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropZone.addEventListener(eventName, preventDefaults, false);
        document.body.addEventListener(eventName, preventDefaults, false);
    });

    ['dragenter', 'dragover'].forEach(eventName => {
        dropZone.addEventListener(eventName, highlight, false);
    });

    ['dragleave', 'drop'].forEach(eventName => {
        dropZone.addEventListener(eventName, unhighlight, false);
    });

    dropZone.addEventListener('drop', handleDrop, false);
    fileInput.addEventListener('change', (e) => handleFilesArray([...e.target.files]), false);

    // Ctrl+V Paste Listener
    document.addEventListener('paste', (e) => {
        if (!e.clipboardData) return;
        const items = e.clipboardData.items;
        const files = [];
        for (let i = 0; i < items.length; i++) {
            if (items[i].type.indexOf('image') !== -1) {
                const file = items[i].getAsFile();
                if (file) files.push(file);
            }
        }
        if (files.length > 0) {
            e.preventDefault();
            handleFilesArray(files);
        }
    });

    function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
    }

    function highlight(e) {
        dropZone.classList.add('drag-over');
    }

    function unhighlight(e) {
        dropZone.classList.remove('drag-over');
    }

    function handleDrop(e) {
        handleFilesArray([...e.dataTransfer.files]);
    }

    // Core file handler (sorts and queues)
    function handleFilesArray(filesArray) {
        // Filter out non-images and sort alphabetically by filename
        const validFiles = filesArray
            .filter(f => f.type.startsWith('image/'))
            .sort((a, b) => a.name.localeCompare(b.name));

        if (validFiles.length === 0) {
            showStatus('No valid images found.', 'error');
            return;
        }

        uploadQueue = uploadQueue.concat(validFiles);
        // Re-sort the queue in case multiple batches were dropped/pasted before processing finished
        uploadQueue.sort((a, b) => a.name.localeCompare(b.name));

        updateProgress();
        if (!isUploading) {
            processQueue();
        }
    }

    function updateProgress() {
        const total = uploadQueue.length + uploadResults.length;
        const completed = uploadResults.length;
        if (total > 0) {
            progressDiv.textContent = `Progress: ${completed}/${total} files`;
        } else {
            progressDiv.textContent = '';
        }
    }

    async function processQueue() {
        if (uploadQueue.length === 0) {
            if (uploadResults.length > 0) {
                displayUrls(uploadResults);
                uploadResults = []; // reset for the next batch
            }
            isUploading = false;
            updateProgress();
            return;
        }

        isUploading = true;
        const file = uploadQueue.shift();

        const formData = new FormData();
        formData.append('img', file);
        formData.append('content_type', '0');
        formData.append('max_th_size', '420');

        try {
            await uploadFile(file, formData);
        } catch (error) {
            showStatus(`Error uploading ${file.name}: ${error}`, 'error');
        }

        processQueue();
    }

    function uploadFile(file, formData) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://api.pixhost.to/images',
                data: formData,
                headers: {
                    'Accept': 'application/json'
                },
                onload: async function(response) {
                    try {
                        const data = JSON.parse(response.responseText);

                        if (!data.show_url) {
                            throw new Error(data.error || "API did not return a valid URL.");
                        }

                        const directUrl = await extractDirectUrl(data.show_url);
                        uploadResults.push({
                            name: data.name || file.name,
                            directUrl: directUrl
                        });

                        updateProgress();
                        showStatus(`Uploaded: ${file.name}`, 'success');
                        resolve();
                    } catch (error) {
                        reject(error.message || error);
                    }
                },
                onerror: function(error) {
                    reject(error.statusText);
                }
            });
        });
    }

    function extractDirectUrl(showUrl) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: showUrl,
                onload: function(response) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    const imgElement = doc.querySelector('#image');
                    if (imgElement && imgElement.src) {
                        resolve(imgElement.src);
                    } else {
                        reject('Could not scrape direct image URL');
                    }
                },
                onerror: function(error) {
                    reject(error.statusText);
                }
            });
        });
    }

    function displayUrls(results) {
        const rawUrls = results.map(r => r.directUrl).join('\n');
        const bbcode = results.map(r => `[img]${r.directUrl}[/img]`).join('\n');
        const markdown = results.map(r => `![${r.name}](${r.directUrl})`).join('\n');

        // Create the UI with single textarea and action buttons
        urlOutput.innerHTML = `
            <textarea class="url-textarea" readonly spellcheck="false">${rawUrls}</textarea>
            <div class="action-buttons">
                <button class="copy-btn" data-clipboard-text="${encodeURIComponent(rawUrls)}">Copy URLs</button>
                <button class="copy-btn" data-clipboard-text="${encodeURIComponent(bbcode)}">Copy BBCode</button>
                <button class="copy-btn" data-clipboard-text="${encodeURIComponent(markdown)}">Copy MD</button>
            </div>
        `;

        // Bind copy functionality
        urlOutput.querySelectorAll('.copy-btn').forEach(btn => {
            btn.addEventListener('click', function() {
                const text = decodeURIComponent(this.getAttribute('data-clipboard-text'));
                navigator.clipboard.writeText(text).then(() => {
                    const originalText = this.textContent;
                    this.textContent = 'Copied!';
                    setTimeout(() => {
                        this.textContent = originalText;
                    }, 1200);
                });
            });
        });
    }

    function showStatus(message, type) {
        statusContainer.className = `status ${type}`;
        statusContainer.textContent = message;
        statusContainer.style.display = 'block';

        clearTimeout(statusTimeout);
        statusTimeout = setTimeout(() => {
            statusContainer.style.display = 'none';
        }, 3000);
    }
})();