Skip to content

Instantly share code, notes, and snippets.

@bakape
Forked from an-electric-sheep/meguca.user.js
Last active August 24, 2017 06:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bakape/45507ab226a8b3104814662adccbae85 to your computer and use it in GitHub Desktop.
Save bakape/45507ab226a8b3104814662adccbae85 to your computer and use it in GitHub Desktop.
meguca multi-upload userscript - click RAW to install
// ==UserScript==
// @name meguca multi-upload
// @downloadURL https://gist.github.com/an-electric-sheep/ce0cd642b2bff8508f39931e902588b1/raw/meguca.user.js
// @namespace https://github.com/an-electric-sheep
// @description drag&drop 2 or more files onto meguca to auto-dump
// @include *://meguca.org/*
// @version 2.10
// @grant none
// @run-at document-start
// @noframes
// ==/UserScript==
"use strict";
const queue = [];
const skip = Symbol("skip")
const style = `
.replylinkwrapper {
display: flex; flex-direction:row;
}
.multiupload {z-index: 1;}
.multiupload::before {content: '[';}
.multiupload::after {content: ']';}
.multifilepanel[hidden] {
display:none;
}
.multifilepanel {
position: fixed;
right: 5px;
}
#upload-queue {
overflow-y: auto;
max-height: 70vh;
}
#upload-queue:empty, .multifilepanel:not(.has-items) .queue-controls, .multifilepanel:not(.has-items) #start-dump {
display:none;
}
.queue-controls {
float: right;
}
.queue-controls > a {
margin-right: 0.5em;
}
.multifilepanel svg {
fill: currentColor;
width: 1em;height: 1em;
cursor: pointer;
}
#dump-delay {width: 4em;}
`;
// https://useiconic.com/open/
const icons = {
del: `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<path d="M3 0c-.55 0-1 .45-1 1h-1c-.55 0-1 .45-1 1h7c0-.55-.45-1-1-1h-1c0-.55-.45-1-1-1h-1zm-2 3v4.81c0 .11.08.19.19.19h4.63c.11 0 .19-.08.19-.19v-4.81h-1v3.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-3.5h-1v3.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-3.5h-1z" />
</svg>`,
dedup: `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<path d="M0 0v4h4v-4h-4zm5 2v3h-3v1h4v-4h-1zm2 2v3h-3v1h4v-4h-1z" />
</svg>`,
sort: `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<path d="M2 0v6h-2l2.5 2 2.5-2h-2v-6h-1zm2 0v1h2v-1h-2zm0 2v1h3v-1h-3zm0 2v1h4v-1h-4z" />
</svg>`
};
const panel = `<aside class="multifilepanel" hidden>
Multi-Upload
<div class="queue-controls">
<a id="remove-dupes" title="Remove dupes from current thread">${icons.dedup}</a>
<a id="name-sort" title="Sort by filename">${icons.sort}</a>
<a id="clear-queue" title="Clear Queue">${icons.del}</a>
<input id="dump-delay" type=number min=0 value=31 title="delay between posts in seconds"><label for="dump-delay">s</label>
</div>
<ol id="upload-queue"></ol>
<input id="multi-dump" type="file" multiple>
<button id="start-dump">Start Dump</button>
</aside>`;
document.addEventListener("DOMContentLoaded", init)
let currentThread = null;
function init() {
let stel = document.createElement("style");
stel.textContent = style;
document.head.append(stel)
const obs = new MutationObserver(() => {
let newThread = document.querySelector("#thread-container");
if(newThread !== currentThread) {
currentThread = newThread;
pageNav();
}
});
obs.observe(document.querySelector("#threads"), {childList: true});
pageNav()
}
function pageNav() {
currentThread = document.querySelector("#thread-container");
if(!currentThread)
return;
currentThread.insertAdjacentHTML("beforebegin", panel)
document.querySelector("#remove-dupes").addEventListener("click", removeDupes)
document.querySelector("#start-dump").addEventListener("click", processQueue)
document.querySelector("#clear-queue").addEventListener("click", () => {
queue.length = 0
displayQueue()
})
document.querySelector("#name-sort").addEventListener("click", () => {
queue.sort((a, b) => a.name.localeCompare(b.name, "en", {numeric: true}))
displayQueue()
})
document.querySelector("#multi-dump").addEventListener("change", (e) => {
let el = e.target;
queue.push(...el.files)
el.value = ""
displayQueue()
})
let replyLink = document.querySelector("aside.posting")
let wrapper = document.createElement("div")
wrapper.classList.add("replylinkwrapper")
replyLink.replaceWith(wrapper)
wrapper.append(replyLink)
wrapper.insertAdjacentHTML('beforeend', `<aside class="act glass multiupload"><a>Multi File Upload</a></aside>`)
wrapper.querySelector(".multiupload a").addEventListener("click", () => {
let panel = document.querySelector(".multifilepanel")
panel.hidden = false
panel.querySelector("input[type=file]").click()
})
if(queue.length > 0)
displayQueue()
}
function removeDupes() {
let url = new URL(window.location)
url.pathname = "/json/boards" + url.pathname
let threadinfo = fetch(url).then(rsp => rsp.json())
let hashes = Promise.all(queue.map(f => {
return new Promise((res, rej) => {
let r = new FileReader();
r.readAsArrayBuffer(f);
r.onload = (e) => res(r.result)
}).then((buf) => {
return window.crypto.subtle.digest("SHA-1", buf)
}).then(dig => {
return {file: f,digest: dig};
});
}))
Promise.all([threadinfo, hashes]).then(([ti, hs]) => {
let existing = new Set();
for(let p of ti.posts) {
if(p.image)
existing.add(p.image.SHA1);
}
for(let hashed of hs) {
const id = hex(hashed.digest)
if (existing.has(id)) {
let i = queue.indexOf(hashed.file)
queue.splice(i, 1)
} else {
// Also remove duplicates from the queue
existing.add(id)
}
}
displayQueue()
})
}
// copypasta from MDN
function hex(buffer) {
var hexCodes = [];
var view = new DataView(buffer);
for (var i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
var value = view.getUint32(i)
// toString(16) will give the hex representation of the number without padding
var stringValue = value.toString(16)
// We use concatenation and slice for padding
var padding = '00000000'
var paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
// Join all the hex strings into one
return hexCodes.join("");
}
// https://meguca.org/uploadHash
// postbody = sha1 hash
// returns token if exists
// /json/boards/:board/:thread -> posts -> image -> sha1
function awaitForm() {
let _res;
let p = new Promise((res, rej) => {_res = res;})
let obs = new MutationObserver(() => {
// wait for thumbnail
if(!document.querySelector(".reply-form figure img"))
return;
// then wait for done button
let done = document.querySelector(".reply-form input[name=done]")
if(done && !done.hidden) {
_res(done);
obs.disconnect();
}
});
obs.observe(document.querySelector("#thread-container"), {childList: true, subtree: true});
return p;
}
function awaitReplyClose() {
if(queue.length == 0)
return;
let obs = new MutationObserver(() => {
obs.disconnect();
processQueue();
});
obs.observe(document.querySelector("#thread-container"), {childList: true});
}
function processQueue() {
if(queue.length == 0)
return;
// reply form open?
if(document.querySelector(".reply-form")) {
awaitReplyClose();
return;
}
let file = queue[0];
let newEvent = new DragEvent("drop");
Object.defineProperty(newEvent, "dataTransfer", {
value: {files: [file]}
})
newEvent[skip] = true;
document.querySelector("#threads").dispatchEvent(newEvent);
awaitForm().then((d) => {
d.click()
queue.shift()
displayQueue()
let delay = document.querySelector("#dump-delay").value | 0;
setTimeout(processQueue, delay * 1000);
})
}
function displayQueue() {
let list = document.querySelector("#upload-queue");
while(list.firstChild)
list.firstChild.remove();
for(let f of queue) {
let li = document.createElement("li");
li.insertAdjacentHTML('beforeend', `<a>${icons.del}</a>`)
li.firstChild.addEventListener("click", () => {
queue.splice(queue.indexOf(f), 1)
displayQueue()
})
let input = document.createElement("span")
input.contentEditable = true
input.textContent = f.name
input.addEventListener("keydown", (e) => e.stopPropagation())
input.addEventListener("input", () => {
let oldFile = f;
f = new File([f], input.textContent, {type: f.type})
let idx = queue.indexOf(oldFile);
queue[idx] = f
})
li.append(input)
list.append(li)
}
// toggle controls
let panel = document.querySelector(".multifilepanel")
panel.classList.toggle("has-items", queue.length > 0)
}
function dropHandler(e) {
if(e[skip])
return;
let multifiledrop = !!e.target.closest(".multiupload, .multifilepanel")
let files = e.dataTransfer.files;
if(files.length < 2 && !multifiledrop)
return;
e.preventDefault();
e.stopPropagation();
queue.push(...Array.from(files));
// show on drop
let panel = document.querySelector(".multifilepanel")
panel.hidden = false
// render
displayQueue();
}
document.documentElement.addEventListener("drop", dropHandler, true);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment