PixHost Drag-and-Drop Uploader

Adds drag-and-drop image upload functionality to PixHost with grouped URLs output

  1. // ==UserScript==
  2. // @name PixHost Drag-and-Drop Uploader
  3. // @namespace https://pixhost.to/
  4. // @version 0.1
  5. // @description Adds drag-and-drop image upload functionality to PixHost with grouped URLs output
  6. // @author You
  7. // @match https://pixhost.to/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Add styles
  17. GM_addStyle(`
  18. #custom-upload-zone {
  19. position: fixed;
  20. top: 20px;
  21. right: 20px;
  22. width: 300px;
  23. background: white;
  24. border: 2px solid #ccc;
  25. border-radius: 8px;
  26. padding: 15px;
  27. z-index: 9999;
  28. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  29. }
  30.  
  31. #drop-zone {
  32. border: 2px dashed #ccc;
  33. border-radius: 4px;
  34. padding: 20px;
  35. text-align: center;
  36. margin-bottom: 10px;
  37. background: #f9f9f9;
  38. transition: all 0.3s ease;
  39. }
  40.  
  41. #drop-zone.drag-over {
  42. background: #e1f5fe;
  43. border-color: #2196F3;
  44. }
  45.  
  46. .url-output {
  47. margin-top: 10px;
  48. max-height: 300px;
  49. overflow-y: auto;
  50. }
  51.  
  52. .url-group {
  53. margin-bottom: 15px;
  54. border-bottom: 1px solid #eee;
  55. padding-bottom: 10px;
  56. }
  57.  
  58. .url-type {
  59. font-weight: bold;
  60. margin-bottom: 5px;
  61. }
  62.  
  63. .url-textarea {
  64. width: 100%;
  65. min-height: 100px;
  66. margin: 5px 0;
  67. font-family: monospace;
  68. font-size: 12px;
  69. resize: vertical;
  70. }
  71.  
  72. .copy-btn {
  73. background: #2196F3;
  74. color: white;
  75. border: none;
  76. padding: 5px 10px;
  77. border-radius: 4px;
  78. cursor: pointer;
  79. margin: 2px;
  80. width: 100%;
  81. }
  82.  
  83. .copy-btn:hover {
  84. background: #1976D2;
  85. }
  86.  
  87. .status {
  88. margin-top: 10px;
  89. padding: 10px;
  90. border-radius: 4px;
  91. }
  92.  
  93. .success {
  94. background: #E8F5E9;
  95. color: #2E7D32;
  96. }
  97.  
  98. .error {
  99. background: #FFEBEE;
  100. color: #C62828;
  101. }
  102.  
  103. .progress {
  104. margin-top: 10px;
  105. font-size: 0.9em;
  106. color: #666;
  107. }
  108. `);
  109.  
  110. // Create upload interface
  111. const uploadInterface = document.createElement('div');
  112. uploadInterface.id = 'custom-upload-zone';
  113. uploadInterface.innerHTML = `
  114. <div id="drop-zone">
  115. Drag & Drop Images Here<br>
  116. <small>or click to select files</small>
  117. <input type="file" id="file-input" multiple style="display: none">
  118. </div>
  119. <div class="progress"></div>
  120. <div class="url-output"></div>
  121. `;
  122.  
  123. document.body.appendChild(uploadInterface);
  124.  
  125. // Setup drag and drop handlers
  126. const dropZone = document.getElementById('drop-zone');
  127. const fileInput = document.getElementById('file-input');
  128. const urlOutput = document.querySelector('.url-output');
  129. const progressDiv = document.querySelector('.progress');
  130.  
  131. let uploadQueue = [];
  132. let uploadResults = [];
  133. let isUploading = false;
  134.  
  135. dropZone.addEventListener('click', () => fileInput.click());
  136.  
  137. ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  138. dropZone.addEventListener(eventName, preventDefaults, false);
  139. document.body.addEventListener(eventName, preventDefaults, false);
  140. });
  141.  
  142. ['dragenter', 'dragover'].forEach(eventName => {
  143. dropZone.addEventListener(eventName, highlight, false);
  144. });
  145.  
  146. ['dragleave', 'drop'].forEach(eventName => {
  147. dropZone.addEventListener(eventName, unhighlight, false);
  148. });
  149.  
  150. dropZone.addEventListener('drop', handleDrop, false);
  151. fileInput.addEventListener('change', handleFiles, false);
  152.  
  153. function preventDefaults(e) {
  154. e.preventDefault();
  155. e.stopPropagation();
  156. }
  157.  
  158. function highlight(e) {
  159. dropZone.classList.add('drag-over');
  160. }
  161.  
  162. function unhighlight(e) {
  163. dropZone.classList.remove('drag-over');
  164. }
  165.  
  166. function handleDrop(e) {
  167. const dt = e.dataTransfer;
  168. const files = dt.files;
  169. handleFiles({ target: { files: files } });
  170. }
  171.  
  172. function handleFiles(e) {
  173. const files = [...e.target.files];
  174. uploadQueue = uploadQueue.concat(files);
  175. updateProgress();
  176. if (!isUploading) {
  177. processQueue();
  178. }
  179. }
  180.  
  181. function updateProgress() {
  182. const total = uploadQueue.length + uploadResults.length;
  183. const completed = uploadResults.length;
  184. if (total > 0) {
  185. progressDiv.textContent = `Progress: ${completed}/${total} files`;
  186. } else {
  187. progressDiv.textContent = '';
  188. }
  189. }
  190.  
  191. async function processQueue() {
  192. if (uploadQueue.length === 0) {
  193. if (uploadResults.length > 0) {
  194. displayGroupedUrls(uploadResults);
  195. uploadResults = [];
  196. }
  197. isUploading = false;
  198. updateProgress();
  199. return;
  200. }
  201.  
  202. isUploading = true;
  203. const file = uploadQueue.shift();
  204.  
  205. if (!file.type.startsWith('image/')) {
  206. showStatus(`${file.name} is not an image file`, 'error');
  207. processQueue();
  208. return;
  209. }
  210.  
  211. const formData = new FormData();
  212. formData.append('img', file);
  213. formData.append('content_type', '0');
  214. formData.append('max_th_size', '420');
  215.  
  216. try {
  217. await uploadFile(file, formData);
  218. } catch (error) {
  219. showStatus(`Error uploading ${file.name}: ${error}`, 'error');
  220. }
  221.  
  222. processQueue();
  223. }
  224.  
  225. function uploadFile(file, formData) {
  226. return new Promise((resolve, reject) => {
  227. GM_xmlhttpRequest({
  228. method: 'POST',
  229. url: 'https://api.pixhost.to/images',
  230. data: formData,
  231. headers: {
  232. 'Accept': 'application/json'
  233. },
  234. onload: async function(response) {
  235. try {
  236. const data = JSON.parse(response.responseText);
  237. const directUrl = await extractDirectUrl(data.show_url);
  238. uploadResults.push({
  239. name: data.name,
  240. directUrl: directUrl,
  241. showUrl: data.show_url
  242. });
  243. updateProgress();
  244. showStatus(`${file.name} uploaded successfully!`, 'success');
  245. resolve();
  246. } catch (error) {
  247. reject(error);
  248. }
  249. },
  250. onerror: function(error) {
  251. reject(error.statusText);
  252. }
  253. });
  254. });
  255. }
  256.  
  257. function extractDirectUrl(showUrl) {
  258. return new Promise((resolve, reject) => {
  259. GM_xmlhttpRequest({
  260. method: 'GET',
  261. url: showUrl,
  262. onload: function(response) {
  263. const parser = new DOMParser();
  264. const doc = parser.parseFromString(response.responseText, 'text/html');
  265. const imgElement = doc.querySelector('#image');
  266. if (imgElement && imgElement.src) {
  267. resolve(imgElement.src);
  268. } else {
  269. reject('Could not find direct image URL');
  270. }
  271. },
  272. onerror: function(error) {
  273. reject(error.statusText);
  274. }
  275. });
  276. });
  277. }
  278.  
  279. function displayGroupedUrls(results) {
  280. const urlGroup = document.createElement('div');
  281. urlGroup.className = 'url-group';
  282.  
  283. const formats = {
  284. 'Direct URLs': results.map(r => r.directUrl).join('\n'),
  285. 'BBCode': results.map(r => `[img]${r.directUrl}[/img]`).join('\n'),
  286. 'Markdown': results.map(r => `![${r.name}](${r.directUrl})`).join('\n')
  287. };
  288.  
  289. Object.entries(formats).forEach(([formatType, text]) => {
  290. const formatSection = document.createElement('div');
  291. formatSection.innerHTML = `
  292. <div class="url-type">${formatType}</div>
  293. <textarea class="url-textarea" readonly>${text}</textarea>
  294. <button class="copy-btn" data-clipboard-text="${text}">Copy ${formatType}</button>
  295. `;
  296. urlGroup.appendChild(formatSection);
  297. });
  298.  
  299. // Clear previous results
  300. urlOutput.innerHTML = '';
  301. urlOutput.appendChild(urlGroup);
  302.  
  303. // Add click handlers for copy buttons
  304. urlGroup.querySelectorAll('.copy-btn').forEach(btn => {
  305. btn.addEventListener('click', function() {
  306. const text = this.getAttribute('data-clipboard-text');
  307. navigator.clipboard.writeText(text).then(() => {
  308. const originalText = this.textContent;
  309. this.textContent = 'Copied!';
  310. setTimeout(() => {
  311. this.textContent = originalText;
  312. }, 1000);
  313. });
  314. });
  315. });
  316. }
  317.  
  318. function showStatus(message, type) {
  319. const statusDiv = document.createElement('div');
  320. statusDiv.className = `status ${type}`;
  321. statusDiv.textContent = message;
  322. urlOutput.insertBefore(statusDiv, urlOutput.firstChild);
  323.  
  324. setTimeout(() => {
  325. statusDiv.remove();
  326. }, 3000);
  327. }
  328. })();