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); }) } })();