Drag-and-drop & Ctrl+V
// ==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 => ``).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);
}
})();