NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name OpenEOS Editor Extensions // @namespace OpenEOS editor extensions // @match https://milovana.com/eos/editor/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @license MIT // @version 1.8.5 // @author fapnip // @description Adds additional fields to the EOS editor to support OpenEOS features: https://milovana.com/forum/viewtopic.php?f=26&t=23558 // ==/UserScript== let lastPageText = ""; const oeosUrlBase = "https://oeos.art/"; onRequestAnimationFrame(); const oeosUrlMatcher = /^file:.+(\+\(\|(oeos|oeos-video):(.*)\))$/ const allowOeosUrlMatcher = /^file:/ const allowedUrlMatcher = /(^(https:\/\/thumbs[0-9]*\.*gfycat\.com\/.+|https:\/\/thumbs[0-9]*\.*redgifs\.com\/.+|https:\/\/w*[0-9]*\.*mboxdrive\.com\/.+|https:\/\/media[0-9]*\.vocaroo\.com\/.+|https:\/\/iili\.io\/.+|https:\/\/i\.ibb\.co\/.+|https:\/\/media\.milovana\.com\/.+)|^(file:|gallery:).*\+\(\|(oeos|oeos-video):(.+)\)$)/ const gifUrlStart = '+(|oeos:' const vidUrlStart = '+(|oeos-video:' const gifUrlEnd = ')' const redGifsWatchMatch = /^(https:\/\/|.*)(www\.|.*)(redgifs|gfycat)\.com\/(watch\/|)([a-zA-Z]+)/ const vids = [] const loadScriptQuery = ` query LoadScript($teaseId: ID!) { tease(id: $teaseId) { id title privateShareUrl ... on EosTease { script } } } ` let lastTeaseId = null let lastTeaseKey = null let lastTeaseScript = null // TODO: Figure out some mutation observers to use instead of requestAnimationFrame loop function onRequestAnimationFrame() { requestAnimationFrame(onRequestAnimationFrame); const loader = document.getElementsByClassName("MuiCircularProgress-root")[0]; if (loader !== undefined) { return; } const page = document.querySelector("#root > div > main > header > div > h6"); if (page && page.textContent !== lastPageText) { lastPageText = page.textContent; onPageEnter(page.textContent, page); } } function getReactInternalInstance(node) { for (const key in node) { if (key.startsWith('__reactInternalInstance$')) { return node[key] } } return null } function getCurrentTeaseId() { const matches = window.location.href.match(/\/eos\/editor\/([0-9]+)\//) if (!matches) { return null } return matches[1] } function getKeyFromTeaseUrl(url) { const matches = url.match(/&key=([a-z0-9]+)/) if (matches) { return matches[1] } return null } function getCurrentTeaseKey(onContinue, onError) { const currentTeaseId = getCurrentTeaseId() if (!currentTeaseId) { onError && onError() return } if (currentTeaseId !== lastTeaseId) lastTeaseKey = null lastTeaseId = currentTeaseId if (!lastTeaseKey) { const query = { operationName: 'LoadScript', variables: { teaseId: currentTeaseId }, query: loadScriptQuery } GM_xmlhttpRequest({ method: 'POST', url: '/graphql/', data: JSON.stringify(query), headers: { 'Content-Type': 'application/json' }, onload: response => { try { const result = JSON.parse(response.responseText) lastTeaseScript = result lastTeaseKey = getKeyFromTeaseUrl(result.data.tease.privateShareUrl) onContinue && onContinue(lastTeaseKey, lastTeaseScript) } catch (e) { console.error(e) onError && onError() } } }) } else { onContinue && onContinue(lastTeaseKey, lastTeaseScript) } } const loadPreviewInTab = () => { getCurrentTeaseKey(teaseKey => { const teasUrl = oeosUrlBase + `?id=${getCurrentTeaseId()}&key=${teaseKey}&preview=1` window.open(teasUrl, 'oeos_preview_' + getCurrentTeaseId()); }) } GM_addStyle(` #root > div > main > div > div > div.MuiGrid-root.MuiGrid-item.MuiGrid-grid-xs-12.MuiGrid-grid-md-7.MuiGrid-grid-lg-6 > div > div.MuiCardActions-spacing + div { padding-top: 0 !important; height: 60% !important; } #root > div > main > div > div > div.MuiGrid-root.MuiGrid-item.MuiGrid-grid-xs-12.MuiGrid-grid-md-7.MuiGrid-grid-lg-6 > div > div > img { display: block; top: 50% !important; left: 50% !important; max-width: 100% !important; width: auto !important; transform: translate(-50%, -50%) !important; object-fit: contain; } .oeos-hide { display:none !important; } .oeos-preview-switch { margin-left: 10px; } #root.oeos-preview-enabled .oeos-preview-switch span.oeos-show-oeos { display: none; } #root:not(.oeos-preview-enabled) .oeos-preview-switch span.oeos-show-eos { display: none; } #root:not(.oeos-preview-enabled) iframe#oeos-preview { display: none; } #root.oeos-preview-enabled iframe:not(#oeos-preview) { display: none; } `) function onPageEnter(pageTitle, pageHeaderEl) { const actionTitle = document.querySelector("#root > div > main > div > div > div.MuiGrid-root.MuiGrid-item.MuiGrid-grid-xs-12.MuiGrid-grid-md-7.MuiGrid-grid-lg-6 > div > div.MuiCardHeader-root > div.MuiCardHeader-content > span"); let previewModeEl = document.querySelector("#oeosPreviewMode"); if (previewModeEl) { previewModeEl.classList.add('oeos-hide') } if (actionTitle !== null && actionTitle.textContent && actionTitle.textContent.match(/^(Image$|Audio)/)) { const isVideo = !!actionTitle.textContent.match(/^(Audio)/); const form = document.querySelector("#root > div > main > div > div > div.MuiGrid-root.MuiGrid-item.MuiGrid-grid-xs-12.MuiGrid-grid-md-7.MuiGrid-grid-lg-6 > div > div.MuiCardContent-root > div"); const formInput = form.querySelector("input"); const buttonContainer = document.querySelector("#root > div > main > div > div > div.MuiGrid-root.MuiGrid-item.MuiGrid-grid-xs-12.MuiGrid-grid-md-7.MuiGrid-grid-lg-6 > div > div.MuiCardActions-root.MuiCardActions-spacing"); const formInputReact = getReactInternalInstance(formInput); const clickPosition = document.createElement('div'); const vidContainer = document.createElement('div'); vidContainer.classList.add('oeos-hide'); vidContainer.innerHTML = `<video class="MuiCardMedia-media" controls="" loop="" style=" position: relative; transform: translate(-50%, 0) !important; top: 0 !important; height: 100%; display: block; left: 50% !important; max-width: 100% !important; width: auto !important; object-fit: contain;"> <source src="" type="video/mp4"> Your browser does not support the video tag. </video>`; const vidElement = vidContainer.getElementsByTagName('video')[0] const vidSource = vidContainer.getElementsByTagName('source')[0] const gifLabelText = isVideo ? 'OpenEOS video URL' : 'OpenEOS Gif/Webp URL' vids.forEach(v => { const vs = v && v.getElementsByTagName('source') if (vs && vs.length) { vs[0].src = '' vs[0].parentNode.load() v.remove() } }) vids.length = 0 if (isVideo) { buttonContainer.parentNode.insertBefore(vidContainer, buttonContainer.nextSibling); vids.push(vidContainer) } // if (!buttonContainer) { // // console.warn('buttonContainer', buttonContainer) // requestAnimationFrame(() => {onPageEnter(pageTitle, pageHeaderEl)}) // return // } buttonContainer.insertAdjacentElement('afterbegin', clickPosition) let img; const getImg = () => { if (!img) { img = document.querySelector("#root > div > main > div > div > div.MuiGrid-root.MuiGrid-item.MuiGrid-grid-xs-12.MuiGrid-grid-md-7.MuiGrid-grid-lg-6 > div > div > img"); img.addEventListener("click", (e) => { const rect = e.target.getBoundingClientRect() const x = e.clientX - rect.left //x position within the element. const y = e.clientY - rect.top //y position within the element. const relX = x / e.target.clientWidth // between 0 and 1, where clicked const relY = y / e.target.clientHeight // between 0 and 1, where clicked console.log(e.clientX, e.clientY, rect) clickPosition.innerHTML = `X:${relX.toFixed(2)} (${Math.floor(relX * 100)}%), Y:${relY.toFixed(2)} (${Math.floor(relY * 100)}%)` }); } } const formGif = form.cloneNode(true); const formGifInput = formGif.querySelector("input"); const formGifInputRoot = formGif.querySelector(".MuiInput-root"); const formGifLabel = formGif.querySelector("label"); formGifLabel.classList.add('MuiInputLabel-shrink') formGifLabel.classList.add('MuiFormLabel-filled') const triggerReactUpdate = () => { // Pretend we did a change in react const evt = document.createEvent('HTMLEvents') evt.initEvent("change", false, true) formInput.dispatchEvent(evt) formInputReact.memoizedProps.onChange(evt) } const resoveOeosUrl = oeosRawUrl => { if (oeosRawUrl.startsWith('[')) { // Is json array let arr try { arr = JSON.parse(oeosRawUrl) } catch (e) { throw new Error('Invalid JSON array: ' + oeosRawUrl) } if (!arr || !arr.length) { throw new Error('Empty or Invalid JSON array: ' + oeosRawUrl) } return resoveOeosUrl(arr[Math.floor(Math.random() * arr.length)]) } else { if (!oeosRawUrl || !oeosRawUrl.match(allowedUrlMatcher)) { throw new Error('Invalid or Disallowed OEOS Image URL') } return oeosRawUrl } } const setImage = () => { if (!isVideo) { getImg() } const im = isVideo ? vidSource : img if (im) { if (!im._origSrc) im._origSrc = im.src let oeosRawUrl = formGifInput.value let error = '' if (oeosRawUrl) { try { oeosRawUrl = resoveOeosUrl(oeosRawUrl) } catch (e) { error = e.toString() } if (im.src !== oeosRawUrl) { clickPosition.innerHTML = '' im.src = oeosRawUrl im._lastOeos = oeosRawUrl } } else { if (im.src && (im.src.match(/^https:\/\/media\.milovana\.com\/timg\/tb_/) || (isVideo && im.src.match(/^https:\/\/media\.milovana\.com\//))) && im.src !== im._lastOeos) { im._origSrc = im.src } im._lastOeos = null if (im.src !== im._origSrc) { clickPosition.innerHTML = '' im.src = im._origSrc triggerReactUpdate() // Make sure it's right } } if (error) { formGifLabel.textContent = error; console.error(error) formGifInputRoot.classList.add('Mui-error') formGifLabel.classList.add('Mui-error') } else { formGifLabel.textContent = gifLabelText; formGifInputRoot.classList.remove('Mui-error') formGifLabel.classList.remove('Mui-error') } if (isVideo) { if (!oeosRawUrl || error) { if (im) { im.src = '' } vidContainer.classList.add('oeos-hide') const rgwm = oeosRawUrl && oeosRawUrl.match(redGifsWatchMatch) // console.log('rgwm', rgwm, oeosRawUrl) if (rgwm) { const rgwmType = rgwm[3] const rgwmId = rgwm[5] let rgurl = ''; if (rgwmType === 'gfycat') { rgurl = 'https://gfycat.com/' + rgwmId } else { rgurl = 'https://www.redgifs.com/watch/' + rgwmId } // console.log('Doing RGWM', rgurl) formGifLabel.textContent = 'Looking up mp4 URL from ' + rgwmType + ' watch URL...'; GM_xmlhttpRequest({ method: 'GET', url: rgurl, // headers: { // 'Content-Type': 'application/json' // }, onload: response => { // console.log('Got RGWM Response', response) let error = '' try { let vmatR = '' if (rgwmType === 'gfycat') { vmatR = new RegExp('https://thumbs[0-9]*\\.gfycat\\.com/' + rgwmId + '-mobile\\.mp4', 'i') } else { vmatR = new RegExp('https://thumbs[0-9]*\\.redgifs\\.com/' + rgwmId + '-mobile\\.mp4', 'i') } const vmat = response.responseText.match(vmatR) // console.log('vmatR', vmatR, vmat, response.responseText) if (vmat) { formGifInput.value = vmat[0] updateEosUrl() } else { error = 'Unable to get video URL from given ' + rgwmType + ' watch link' } } catch (e) { console.error(e) error = 'Error trying to get video URL from given ' + rgwmType + ' watch link' } if (error) { formGifLabel.textContent = error; console.error(error) formGifInputRoot.classList.add('Mui-error') formGifLabel.classList.add('Mui-error') } } }) } } else { vidContainer.classList.remove('oeos-hide') } vidElement.load() } else { if (oeosRawUrl && error) { const rgimgbb = oeosRawUrl && oeosRawUrl.match(/^(https:\/\/|.*)(www\.|)ibb\.co\/([a-zA-Z0-9]+)\s*$/) // console.log('rgwm', rgwm, oeosRawUrl) if (rgimgbb) { const imgbbId = rgimgbb[3] const imgbbUrl = oeosRawUrl // console.log('Doing RGWM', rgurl) formGifLabel.textContent = 'Looking up image URL from ImgBB URL...'; GM_xmlhttpRequest({ method: 'GET', url: imgbbUrl, // headers: { // 'Content-Type': 'application/json' // }, onload: response => { // console.log('Got IMGBB Response', response) let error = '' try { const imatR = /<link rel="image_src" href="(https:\/\/[^"]+?)">/ const imat = response.responseText.match(imatR) // console.log('vmatR', vmatR, vmat, response.responseText) if (imat) { formGifInput.value = imat[1] updateEosUrl() } else { error = 'Unable to get image URL from given link' } } catch (e) { console.error(e) error = 'Error trying to get image URL from given link' } if (error) { formGifLabel.textContent = error; console.error(error) formGifInputRoot.classList.add('Mui-error') formGifLabel.classList.add('Mui-error') } } }) } } } } } const updateOeosUrl = () => { const matches = formInput.value.match(oeosUrlMatcher) if (matches) { const oeosUrl = decodeURIComponent(matches[3]) if (oeosUrl !== formGifInput.value) { formGifInput.value = oeosUrl } } else { formGifInput.value = '' } if (formInput.value.match(allowOeosUrlMatcher)) { formGif.style.display = 'inline-flex' } else { formGif.style.display = 'none' } setImage() } const updateEosUrl = () => { const matches = formInput.value.match(oeosUrlMatcher) const oeosRawUrl = formGifInput.value const oeosUrl = encodeURIComponent(oeosRawUrl) const fullOeosUrl = (isVideo ? vidUrlStart : gifUrlStart) + oeosUrl + gifUrlEnd let didChange = false if (!oeosUrl) { if (matches) { formInput.value = formInput.value.replace(matches[1], '') didChange = true } } else { if (matches) { if (matches[1] !== fullOeosUrl) { formInput.value = formInput.value.replace(matches[1], fullOeosUrl) didChange = true } } else { formInput.value = formInput.value + fullOeosUrl didChange = true } } if (didChange) { triggerReactUpdate() } if (formInput.value.match(allowOeosUrlMatcher)) { formGif.style.display = 'inline-flex' } else { formGif.style.display = 'none' isVideo && vidContainer.classList.add('oeos-hide') } setImage() } form.parentNode.appendChild(formGif); formGifLabel.textContent = gifLabelText; formGifInput.value = ""; const updateEosForm = () => { updateEosUrl(); } formGifInput.addEventListener("input", updateEosForm); // Monitor change of EOS url const observer = new MutationObserver(m => { updateOeosUrl() }) observer.observe(formInput, {attributes: true}) updateOeosUrl() // Force update of OEOS url updateEosUrl() } if (pageTitle === "Share") { const form = document.querySelector("#root > div > main > div > div > div > div"); if (form === null) { return; } const linkLabel = 'OpenEOS Link'; const formOpenEOS = form.cloneNode(true); const formOpenEOSInput = formOpenEOS.querySelector("input"); const formOpenEOSLabel = formOpenEOS.querySelector("label"); const formOpenEOSLegend = formOpenEOS.querySelector("legend span"); let link = formOpenEOS.value; formOpenEOSInput.value = formOpenEOSInput.value.replace("https://milovana.com/webteases/showtease.php", oeosUrlBase); formOpenEOSLabel.innerHTML = linkLabel; formOpenEOSLegend.innerHTML = linkLabel; form.parentNode.appendChild(formOpenEOS); formOpenEOS.onclick = () => { formOpenEOSInput.select(); document.execCommand("copy"); } } // Current host for OpenEOS can't be configured to allow OpenEOS to run in iframe. We'll just provide a button to open in a new tab, for now. if (pageTitle === "Preview") { if (!previewModeEl) { previewModeEl = document.createElement('div') previewModeEl.id = 'oeosPreviewMode' previewModeEl.innerHTML = `<button class="oeos-preview-switch" tabindex="0" type="button"> <span class="MuiButton-label"><span class="oeos-show-oeos">Preview in OpenEOS</span></span><span class="MuiTouchRipple-root"></span> </button>` const previewSwitchButton = previewModeEl.querySelector('.oeos-preview-switch') previewSwitchButton.addEventListener('click', () => { loadPreviewInTab() }) } pageHeaderEl.parentNode.insertBefore(previewModeEl, pageHeaderEl.nextSibling) previewModeEl.classList.remove('oeos-hide') } // Current host for OpenEOS can't be configured to allow OpenEOS to run in iframe. We'll disable OpenEOS ifrmae embedding, for now // if (pageTitle === "Preview") { // const rootEl = document.querySelector('#root') // const eosIframe = rootEl.querySelector('iframe:not(#oeos-preview)') // let oeosIframe = rootEl.querySelector('iframe#oeos-preview') // const loadPreview = () => { // if (!oeosIframe) { // getCurrentTeaseKey(teaseKey => { // const teasUrl = oeosUrlBase + `?id=${getCurrentTeaseId()}&key=${teaseKey}` // oeosIframe = document.createElement('iframe') // oeosIframe.classList.add(...eosIframe.classList) // oeosIframe.id = 'oeos-preview' // oeosIframe.src = teasUrl // eosIframe.parentNode.insertBefore(oeosIframe, eosIframe.nextSibling) // }) // } // } // if (!previewModeEl) { // previewModeEl = document.createElement('div') // previewModeEl.id = 'oeosPreviewMode' // previewModeEl.innerHTML = `<button class="oeos-preview-switch" tabindex="0" type="button"> // <span class="MuiButton-label"><span class="oeos-show-oeos">Switch to OpenEOS</span><span class="oeos-show-eos">Switch to Standard EOS</span></span><span class="MuiTouchRipple-root"></span> // </button>` // const previewSwitchButton = previewModeEl.querySelector('.oeos-preview-switch') // previewSwitchButton.addEventListener('click', () => { // if (rootEl.classList.contains('oeos-preview-enabled')) { // rootEl.classList.remove('oeos-preview-enabled') // } else { // rootEl.classList.add('oeos-preview-enabled') // } // if (rootEl.classList.contains('oeos-preview-enabled')) loadPreview() // getCurrentTeaseKey() // }) // } // pageHeaderEl.parentNode.insertBefore(previewModeEl, pageHeaderEl.nextSibling) // previewModeEl.classList.remove('oeos-hide') // if (rootEl.classList.contains('oeos-preview-enabled')) loadPreview() // } }