NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name EOS Dynamic Images
// @namespace EOS Module - Dynamic Images
// @description Add dynamic images to EOS
// @version 0.8
// @license MIT
// @include https://eosscript.com/*
// @include https://milovana.com/webteases/*
// @include https://milovana.com/eos/*
// @inject-into auto
// @grant GM_addScript
// @grant GM_xmlhttpRequest
// @run-at document-start
// @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
// ==/UserScript==
/***************************
* READ ME:
* Make sure you change @name, @namespace, @description and var name to something unique to you!
*
* Make sure you keep your module names namespace unique as well.
*
*/
// Module specific code. Just about everything here should be changed for your specific use case!
const DEBUG = true;
let slides = false;
let slide = 0;
let slideTime = 10000;
let slideLoop = false;
let slideTimeout;
// Startup stuff
function moduleInit() {
localEvents.addEventListener('preload-start', () => {
console.warn('Image Preload Starting');
}, false);
localEvents.addEventListener('preload-end', () => {
console.warn('Image Preload Ended');
}, false);
}
const modules = {
dynamicImage: {
version() {
return interpreter.nativeToPseudo(GM.info.script.version);
},
load(locator) {
// Dump what we got
if (DEBUG) console.warn('load Got Value:', locator, getEOSHost());
setImageFromLocator(locator);
},
preload(locator) {
// Dump what we got
if (DEBUG) console.warn('preload Got Value:', getEOSRoot(), locator, getEOSHost());
setImageFromLocator(locator, true);
},
startSlides(locators, time, loop) {
if (DEBUG) console.warn('startSlides Got Value:', locators, time, loop);
if (typeof locators === 'object') {
locators = locators.properties
}
if (typeof locators === 'string') {
locators = [locators]
}
if (!locators || !locators.length) {
console.error('Invalid array of images', locators)
return
}
slides = locators;
slide = 0;
slideLoop = !!loop;
slideTime = time || 10000
if (slideTimeout) clearTimeout(slideTimeout);
nextSlide ();
},
getImageState(locator) {
return interpreter.nativeToPseudo(imageLoadState[locator]);
},
stopSlides() {
slides = false;
},
pauseSlides() {
if (slideTimeout) clearTimeout(slideTimeout);
},
consintueSlides() {
nextSlide();
},
nextSlide() {
nextSlide();
},
setBackgroundColor () {
},
},
// More modules ...
};
// Actions that will run on child (eosscripts.com, iframe)
const childActions = {
// More childActions ...
};
// Actions that will run on parent (milovana.com, parent window)
const parentActions = {
// More parentActions ...
};
function nextSlide () {
if (slides) {
setImageFromLocator(slides[slide]);
dispatchEOSEvent('dynamicImage', 'slidetick');
slide ++;
if (slide >= slides.length) {
slide = 0;
}
if (slide < slides.length) {
slideTimeout = setTimeout(nextSlide, slideTime);
}
}
}
/****************************************************************
* Common Module Injector Operations / Core EOS Userscript
* **************************************************************
* !!!! Shouldn't need to change anything below here !!!!
* (If you do, it'll make it more difficult to keep up-to-date with the core EOS userscript.)
*
* **************************************************************
*/
const name = GM.info.script.name;
if (DEBUG) console.warn(`Installing ${name} v${GM.info.script.version} on ${window.location.host}`);
let interpreter;
let host;
let hooked = false;
let preLoading = 0;
const protos = {};
const imageCache = {};
const lastRandomImage = {};
const imageLoadState = {};
const localEvents = document.createElement('div');
const preloadStartEvent = new Event('preload-start');
const preloadEndEvent = new Event('preload-end');
const eosContainerId = "#eosContainer";
if (window.location.host.match(/eosscript\.com/)) {
// EOS Script site
let origInterpreterRun;
// Wait for EOS container
function install () {
// Our EOS container was created. Should be able to hook the Interpereter now.
if (DEBUG) console.log('Hooking Interpereter Run');
origInterpreterRun = window.Interpreter.prototype.run;
// Hook JS Interpreter's run method
window.Interpreter.prototype.run = function(...args) {
// Wait until PageManager property exists so we know EOS has added its methods
if (!this[`_RUN_START_${name}`] && this.globalObject.properties.PageManager) {
// Now inject our methods into the Interpreter
this[`_RUN_START_${name}`] = true; // And make sure we don't do it again.
if (DEBUG) console.log('Intercepting Interpereter Run');
console.log(`Installing ${name} modules...`);
interpreter = this;
addObjectToInterpreter(interpreter.globalObject, modules);
if (moduleInit) moduleInit();
}
return origInterpreterRun.apply(this, args);
}
}
if (!document.getElementById(eosContainerId)) {
if (DEBUG) console.log('Waiting for eosContainer...');
document.arrive(eosContainerId, function() {
install();
});
} else {
// Already here?
install();
}
// Listen for events for parent<->child messages
window.addEventListener("message", function({data}) {
if (data && data.source === name && childActions[data.action]) {
if (DEBUG) console.log('Running child action', data.action, data);
childActions[data.action].apply(this, data.values || [])
}
}, false);
} else {
// Milovana.com?
// Wait for iframe to arrive:
// watch for element creation in the whole HTML document
document.arrive(".eosIframe", function() {
// For firefox, we need to remove the sandbox attribute, else violentmonkey wont see the iframe.
this.removeAttribute("sandbox");
// Listen for events for parent<->child messages
window.addEventListener("message", function({data}) {
if (data && data.source === name && parentActions[data.action]) {
if (DEBUG) console.log('Running parent action', data.action, data);
parentActions[data.action].apply(this, data.values || [])
}
}, false);
});
}
function randomIntFromInterval(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min);
}
function base64ImageType (string) {
return typeof string === 'string' && string.match(/^data:(image\/[a-z]+);base64/i);
}
function b64toBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1]);
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: base64ImageType(dataURI)[1] });
}
function addImageToCache (name, uri) {
if (DEBUG) console.log('Adding Image from cache:', name, uri);
imageCache[name] = uri;
}
function getImageFromCache (name) {
if (DEBUG && imageCache[name]) console.log('Getting Image from cache:', name, imageCache[name]);
return imageCache[name];
}
function setImageFromLocator(locator, noDisplay) {
if (locator instanceof Blob) {
setImage(locator, locator, null, noDisplay);
}
if (typeof locator !== 'string') {
console.error('Invalid image locator:', locator);
return
}
if (base64ImageType(locator)) {
return locator;
}
if (noDisplay && isPreloaded(locator)) return;
const galleryM = locator.match(/^gallery:([^\/]+)\/(.*)$/)
let image;
if (galleryM) {
// Gallery image
// if (DEBUG) console.log('Looking for gallery:', galleryM[1], getEOSHost())
const gallery = getEOSSript().galleries && getEOSSript().galleries[galleryM[1]];
if (!gallery){
console.error('Invalid Gallery: ' + galleryM[1], getEOSSript().galleries)
return
}
if (galleryM[2] === '*') {
// Random image from gallery
const images = gallery.images;
if (noDisplay) {
// Preloading
for (let i0 = 0, l0 = images.length; i0 < l0; i0++) {
if (DEBUG) console.log('Preloading image: ' + 'gallery:' + galleryM[1] + '/' + images[i0].id)
setImageFromLocator('gallery:' + galleryM[1] + '/' + images[i0].id, noDisplay)
}
return
}
for (let i = 10; i > 0; i--) {
image = images[randomIntFromInterval(0, images.length-1)]
if (image !== lastRandomImage[locator] || matched.length < 3) {
break;
}
}
if (!image) {
console.error('Unable to randomly select image from ' + gallery.name);
return
}
} else {
// Specific image
const imageId = parseInt(galleryM[2], 10)
image = gallery.images.find(i => i.id === imageId)
if (!image) {
console.error('Unable to find imgage ID ' + imageId + ' in ' + gallery.name);
return
}
lastRandomImage[locator] = image
}
} else {
const fileM = locator.match(/^file:(.+)$/)
if (!fileM) {
console.error('Invalid image locator:', locator);
return
}
let fileName;
if (fileM[1].match(/^(.*)\*/)) {
// Random image from wildcard
const fileRegExp = new RegExp(escapeRegExp(fileM[1]).replace(/\\\*/, '.*'));
const files = getEOSSript().files || {};
const matched = Object.keys(files).filter(f => f.match(fileRegExp));
if (noDisplay) {
// Preloading
for (let m1 = 0, l1 = matched.length; m1 < l1; m1++) {
setImageFromLocator('file:' + matched[m1], noDisplay)
}
return
}
for (let i2 = 10; i2 > 0; i2--) {
const randomIndex = randomIntFromInterval(0, matched.length-1);
fileName = matched[randomIndex];
image = files[fileName];
if (image !== lastRandomImage[locator] || matched.length < 3) {
break;
}
}
if (!image) {
console.error('Unable to randomly select image from file pattern: ' + fileM[1]);
return
}
lastRandomImage[locator] = image
} else {
const files = getEOSSript().files
const file = files && files[fileM[1]];
if (!file) {
console.error('Invalid file:', fileM[1]);
return
}
fileName = fileM[1];
image = file
}
console.log('Selecte Image:', image);
if (fileName.match(/\.mp3$/i)) {
// File is an mp3 file. See if we can extract an image from it
const mp3Url = 'https://media.milovana.com/timg/' + image.hash + '.mp3'
const name = 'file:' + fileName;
const cachedImage = getImageFromCache(name);
if (cachedImage) {
setImage(cachedImage, locator, null, noDisplay);
return;
}
// We'll need to ask the parent window to load this since we're restricted.
// runParentAction('__loadImageFromMp3', mp3Url, locator, noDisplay);
loadImageFromMp3(mp3Url, locator, name, noDisplay)
return
}
}
setImage(image, locator, noDisplay ? locator : null, noDisplay);
}
function getFile(url, options) {
let promise = new Promise(function(resolve, reject) {
const opt = {
url: url,
method: 'GET',
responseType: 'blob',
timeout: 10000,
onload: resolve,
onerror: reject,
// ...options
}
try {
GM_xmlhttpRequest(opt);
} catch (e) {
alert('GM_xmlhttpRequest Error');
}
});
return promise;
}
function failedPreload(locator) {
imageLoadState[locator] = false
preLoading --;
if (preLoading < 0) {
console.error('Negative preload counter on failedPreload');
}
if (!preLoading) {
localEvents.dispatchEvent(preloadEndEvent);
}
}
function isPreloaded(locator) {
return !!imageLoadState[locator]
}
function startPreload(locator) {
imageLoadState[locator] = undefined;
if (!preLoading) {
localEvents.dispatchEvent(preloadStartEvent);
}
preLoading ++;
}
function preloadedImage(locator) {
imageLoadState[locator] = true
preLoading --;
if (preLoading < 0) {
console.error('Negative preload counter on preloadedImage');
}
if (!preLoading) {
localEvents.dispatchEvent(preloadEndEvent);
}
}
async function loadImageFromMp3(url, locator, name, noDisplay) {
if (DEBUG) console.log('Downloading MP3 file as image:', url);
try {
if (noDisplay) startPreload(locator);
const response = await getFile(url);
const responseBlob = await response.response;
const blobCheck = responseBlob.slice(0, 90);
const partial = String.fromCharCode.apply(null, new Int8Array(await blobCheck.arrayBuffer()));
const typeCheck = imageTypeFromData(partial);
if (typeCheck) {
// MP3 is actually an image
const imageBlob = new Blob([responseBlob], {type: typeCheck});
if (DEBUG) console.log('Converting MP3', url, 'to', typeCheck, imageBlob.type);
// const imageBlob = responseBlob.slice(0, responseBlob.size, {type: typeCheck});
if (noDisplay) preloadedImage(locator);
setImage(imageBlob, locator, name, noDisplay);
return;
} else {
console.error("Couldn't determine image type of MP3 file", url);
failedPreload(locator);
}
} catch (e) {
console.error('Faild loading image from MP3', url, locator, e);
failedPreload(locator);
setImageFromLocator('gallery:735c4bbe-f318-4993-809a-a71f258f3678/1104250')
}
}
function imageTypeFromData (data) {
let result;
for (let t in imageTypeLookups) {
let l = imageTypeLookups[t];
result = (typeof l.values !== 'string' || data.substr(l.offset, l.values.length) === l.values) &&
(!l.check || l.check(data, l.values, l.offset)) ?
t :
null;
if (result) return result;
}
return result;
}
const imageTypeLookups = {
'image/gif': {
values: String.fromCharCode.apply(null, new Int8Array([0x47, 0x49, 0x46])),
offset: 0
},
'image/webp': {
values: 'WEBP',
offset: 8
},
'image/jpeg': {
values: String.fromCharCode.apply(null, new Int8Array([0xFF, 0xD8, 0xFF])),
offset: 0
},
'image/png': {
values: String.fromCharCode.apply(null, new Int8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])),
offset: 0,
// check: (data, values, offset) => {
// todo: add proper apng/png detection
// },
},
}
function Uint8ToString(u8a){
var CHUNK_SZ = 0x8000;
var c = [];
for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
}
return c.join("");
}
async function setImage(imageObj, locator, name, noDisplay) {
const container = getImageContainer();
if (!noDisplay && !container) {
// Container hasn't been built yet.
// Wait for it.
requestAnimationFrame(() => setImage(imageObj, locator, name, noDisplay))
return;
}
if (typeof imageObj === 'string' && base64ImageType(imageObj)) {
imageObj = b64toBlob(imageObj);
}
if (imageObj instanceof Blob) {
if (!noDisplay) container.src = URL.createObjectURL(imageObj);
if (name) addImageToCache(name, imageObj);
} else {
const url = imageObj.altUrl ? imageObj.altUrl : 'https://media.milovana.com/timg/tb_xl/' + imageObj.hash + '.jpg';
if (!noDisplay) {
container.src = url;
} else if (typeof locator === 'string') {
// pre-load
startPreload(locator);
try {
await getFile(url);
preloadedImage(locator);
} catch (e) {
failedPreload(locator);
}
}
}
}
function getLastTextP () {
const tb = getLastTextBubble()
if (!tb) {
return
}
result = tb.querySelector('p')
console.log('getLastTextP', result);
return result;
}
function getLastTextBubble () {
const result = getBubbleQueue().querySelectorAll('[class^="SingleBubble_root"]');
console.log('getLastTextBubble', result);
if (!result) {
return
}
return result[result.length-1]
}
function getBubbleQueue () {
return getEOSContainer().querySelector('[class^="BubbleQueue_scrollView"]');
}
function getImageContainer () {
return getEOSContainer().querySelector('[class^="Picture_picture"]');
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
function getEOSContainer() {
return document.getElementById('eosContainer');
}
function getEOSRoot() {
return getEOSContainer()._reactRootContainer._internalRoot
}
function getEOSHost() {
if (!host) {
host = getEOSRoot().current.child.stateNode.props.host
}
return host
}
function getEOSSript() {
return getEOSHost().script.getScript();
}
function dispatchEOSEvent(target, type) {
if (!protos[target]) {
console.error('Unable to dispatchEOSEvent. No known proto:', target);
return;
}
getEOSHost().virtualMachine.dispatchEvent({ target: protos[target], type });
}
// Add defined modules / properties / functions to interpereter
function addObjectToInterpreter (base, obj) {
for (const i in obj) {
if (base[i] !== undefined || (base.properties && base.properties[i] !== undefined)) {
console.error(`Property \`${i}\` already exists in object. Unable to add 3rd party module/property`, i, base, base.properties);
continue;
}
const el = obj[i];
if (typeof el === 'object') {
const protoName = i.charAt(0).toUpperCase() + i.slice(1);
if (base[protoName] !== undefined || (base.properties && base.properties[protoName] !== undefined)) {
console.error(`Prototype \`${protoName}\` already exists in object. Unable to add 3rd party module/property`, i, protoName, base, base.properties);
continue;
}
// var container = interpreter.nativeToPseudo({});
// interpreter.setProperty(base, i, container);
const constructor = () => {
throw new Error(`Cannot construct ${protoName} object, use \`${i}\` global`)
};
const constructorf = interpreter.createNativeFunction(constructor, true);
interpreter.setProperty(
constructorf,
'prototype',
interpreter.createObject(interpreter.globalObject.properties['EventTarget']),
Interpreter.NONENUMERABLE_DESCRIPTOR
)
addObjectToInterpreter(constructorf, el);
const protof = constructorf.properties['prototype'];
interpreter.setProperty(interpreter.globalObject, protoName, constructorf);
const proto = interpreter.createObjectProto(protof);
protos[protoName] = proto;
protos[i] = proto;
interpreter.setProperty(base, i, proto);
if (base === interpreter.globalObject) console.log('Loaded 3rd party module:', protoName);
} else if (typeof el === 'function') {
if (base === interpreter.globalObject) {
interpreter.setProperty(base, i, interpreter.createNativeFunction(el));
} else {
interpreter.setNativeFunctionPrototype(base, i, el);
}
} else {
interpreter.setProperty(base, i, el);
}
}
}
function runChildAction(action) {
if (DEBUG) console.log('Requesting child action', action, arguments);
if (!childActions[action]) {
console.error('No child action defined for:', action);
return;
}
const values = [];
for (var i = 1, l = arguments.length; i < l; i++) {
values.push(arguments[i]);
}
const iframes = document.getElementsByClassName('eosIframe');
const message = {
source: name,
action,
values
};
for (var i = 0, l = iframes.length; i < l; i++) {
iframes[i].contentWindow.postMessage(message, '*');
}
}
function runParentAction(action) {
if (DEBUG) console.log('Requesting parent action', action, arguments);
if (!parentActions[action]) {
console.error('No parent action defined for:', action);
return;
}
const values = [];
for (i = 1, l = arguments.length; i < l; i++) {
values.push(arguments[i]);
}
window.parent.postMessage({
source: name,
action,
values
}, '*');
}
// Polyfills
(function () {
File.prototype.arrayBuffer = File.prototype.arrayBuffer || myArrayBuffer;
Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || myArrayBuffer;
function myArrayBuffer() {
// this: File or Blob
return new Promise((resolve) => {
let fr = new FileReader();
fr.onload = () => {
resolve(fr.result);
};
fr.readAsArrayBuffer(this);
})
}
})();