Mismangas Enhanced Reader

Enhanced mismangas.com reader

// ==UserScript==
// @name        Mismangas Enhanced Reader
// @name:es     Mismangas Lector Mejorado
// @namespace   Violentmonkey Scripts
// @match       https://mismangas.com/*
// @grant       none
// @version     1.19.1
// @author      donkikote
// @description Enhanced mismangas.com reader
// @description:es Lector mejorado de mismangas.com
// @license MIT
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.xmlHttpRequest
// ==/UserScript==
const styles = `
    .reader-container {
      padding: 0;
      margin: 0 auto;
    }
    .pages-container {
      display: flex;
      flex-flow: row-reverse wrap;
      justify-content: center;
      column-gap: 5px;
    }
    .pageWrapper {
      position: relative;
      max-width: 100%;
      display: flex;
      justify-content: center;
    }
    @media (max-width: 1200px) {
      .pageWrapper {
        margin-bottom: 50vh;
      }
    }
    .pageFlipButton {
      width: 40%;
      height: 100%;
      position: absolute
    }
    .nextPageBtn {
      left: 0;
    }
    .prevPageBtn {
      right: 0;
    }
    .prevPageBtn:hover {
      background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.3));
    }
    .nextPageBtn:hover {
      background-image: linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0.3));
    }

    .chapter-header {
      display: flex;
      justify-content: space-between;
      padding-right: 1em;
    }

    .nav-insert {
      display: flex;
      flex-grow: 4;
      gap: 1em;
    }
    .disabled ul li {
      opacity: 0.4;
    }
    .enhanced-nav {
      display: flex;
      grow: 4;
      gap: 1em;
    }
`
const chapterUrl = 'https://mismangas.com/capitulo'

const reading_status = 'reading_status'
const last_url = 'last_url'
const updated_at = 'updated_at'
const sync_key = 'sync_key'
const enhanced_nav_id = 'enhanced-nav'

const remoteStorageHost = 'https://keyvalue.immanuel.co/api/KeyVal'
const remoteKeyPrefix = 'MMER_'

const started = 'S'
const read = 'R'

const sleep = ms => new Promise(r => setTimeout(r, ms));
let debounceTimeoutId = null;
const debounce = (callback, wait) => {
  return (...args) => {
    window.clearTimeout(debounceTimeoutId);
    debounceTimeoutId = window.setTimeout(() => {
      callback(...args);
      debounceTimeoutId = null;
    }, wait);
  };
}

let currentData = undefined
let mangaId = undefined
let currentChapter = undefined
let isDoublePage = false


function getBigContainer() {
  return document.getElementsByTagName('main')[0].getElementsByClassName('container-site-web')[0]
}

function getPagesContainer() {
  return document.getElementsByClassName('grid-img-capitulo')[0]
}

function addStyles() {
  var styleSheet = document.createElement('style')
  styleSheet.textContent = styles
  document.head.appendChild(styleSheet)
}

function getSyncKey() {
  return GM_getValue(sync_key)
}

function setSyncKey(value) {
  return GM_setValue(sync_key, value)
}

function getChapterUrl(mangaId, chapterNumber) {
  return `${chapterUrl}/${chapterNumber}/${mangaId}`
}

function addNavUtils() {
  const nav = document.getElementsByTagName('nav')[0].getElementsByTagName('div')[0]
  let enhancedNav = document.getElementById(enhanced_nav_id)
  if (!enhancedNav) {
    enhancedNav = document.createElement('div')
    enhancedNav.id = enhanced_nav_id
    enhancedNav.classList.add('enhanced-nav')

    const syncGroup = document.createElement('form')
    const syncKeyInput = document.createElement('input')
    syncKeyInput.placeholder = 'Sync key'
    const syncKey = getSyncKey()
    if (syncKey) {
      syncKeyInput.value = syncKey
    }
    const syncKeyApplyButton = document.createElement('button')
    syncKeyApplyButton.type = 'submit'
    syncKeyApplyButton.textContent = 'Save'
    syncKeyApplyButton.disabled = true
    const syncKeyAppliedText = document.createElement('span')
    syncKeyAppliedText.textContent = 'Applied!'
    syncKeyAppliedText.style.opacity = 0
    syncKeyInput.addEventListener('input', () => {
      syncKeyApplyButton.disabled = false
    })
    syncGroup.appendChild(syncKeyInput)
    syncGroup.appendChild(syncKeyApplyButton)
    syncGroup.appendChild(syncKeyAppliedText)
    syncGroup.addEventListener('submit', async (e) => {
      e.preventDefault()
      const newSyncKey = syncKeyInput.value

      setSyncKey(newSyncKey)
      if (newSyncKey && mangaId) {
        syncReadingStatus(mangaId)
      }
      syncKeyApplyButton.disabled = false
      syncKeyAppliedText.style.opacity = 1
      await sleep(5000)
      syncKeyAppliedText.style.opacity = 0
      return false
    })


    enhancedNav.appendChild(syncGroup)
    nav.insertBefore(enhancedNav, nav.getElementsByTagName('button')[0])
  }
}

function hideAds() {
  let ads = document.getElementsByClassName('sticky-ad-pop')
  for (let i=0;i<ads.length;i++) {
    let ad = ads[i]
    ad.style.display = 'none'
  }
}

async function resetImageSizes() {
  console.log("Resetting image sizes")
  const container = getBigContainer()
  //bigContainer.style['max-width'] = ''
  const images = await getLoadedImages()
  const viewportSize = getViewportSize()
  for (let image of images) {
    setImageSize(image, viewportSize.vh, viewportSize.vw)
  }
  const commonPageWidth = getCommonPageWidth(images)
  const doublePageWidth = getDoublePageWidth(commonPageWidth)
  container.style.maxWidth = `${doublePageWidth}px`
  isDoublePage = container.getBoundingClientRect().width == doublePageWidth
  document.location.href = document.location.href

}

function setImageSize(image, height, width) {
  image.style['max-height'] = height + 'px'
  image.style['max-width'] = width + 'px'
}

function getViewportSize() {
  return {
    vh: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0),
    vw: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
  }
}

function wrapImages(images) {
  // Wrap page images with anchor that scrolls to next page except last one that redirects to next chapter
  const viewportSize = getViewportSize()
  const imgCount = images.length

  const imageWrappers = []
  for (let i=0;i<imgCount;i++) {
    const image = images[i]
    const wrapper = document.createElement('span')
    wrapper.classList.add('pageWrapper')

    imageWrappers.push(wrapper)
  }

  const pagesContainer = getPagesContainer()
  for (let i=0;i<images.length;i++) {
    const image = images[i]
    image.classList.add('page')
    setImageSize(image, viewportSize.vh, viewportSize.vw)
    image.style.width = 'auto'
    imageWrappers[i].appendChild(image)
    pagesContainer.appendChild(imageWrappers[i])
  }
}

function getCommonPageWidth(images) {
  const imageWidths = {}
  for (let image of images) {
    if (imageWidths[image.width]) {
      imageWidths[image.width] += 1
    } else {
      imageWidths[image.width] = 1
    }
  }
  // Get common page width for double paging
  let commonPageWidth = 0
  let commonPageWidthCount = 0
  for (i in imageWidths) {
    if (imageWidths[i] > commonPageWidthCount) {
      commonPageWidth = i
      commonPageWidthCount = imageWidths[i]
    }
  }
  return commonPageWidth
}

function addPagination(images, commonPageWidth, nextChapterUrl) {
  // Assign ids and add placeholder id for double pagers to keep scrolling consistent
  let j = 0
  const imgCount = images.length
  for (let i=0;i<imgCount;i++,j++) {
    const id = 'page'+j
    const wrapper = images[i].parentNode
    const doublePager = images[i].width != commonPageWidth

    images[i].id = id
    if (doublePager) {
      j++
      images[i].parentNode.id = 'page'+j
    }

    // Add scrolling buttons
    const nextImgAnchor = document.createElement('a')
    nextImgAnchor.classList.add('pageFlipButton')
    nextImgAnchor.classList.add('nextPageBtn')
    if (i == imgCount-1) {
      nextImgAnchor.href = nextChapterUrl
    } else {
      nextImgAnchor.href = '#page'+(j+1)
    }
    wrapper.appendChild(nextImgAnchor)

    if (i > 0) {
      const prevImgAnchor = document.createElement('a')
      prevImgAnchor.classList.add('pageFlipButton')
      prevImgAnchor.classList.add('prevPageBtn')
      prevImgAnchor.href = '#page'+(j - (doublePager ? 2 : 1))
      wrapper.appendChild(prevImgAnchor)
    }
  }
  return j
}

function addKeyboardNavigation(pageCount, nextChapterUrl) {
  // Add lef/right keys navigation
  document.addEventListener('keydown', function(event) {
    const currentUrl = window.location.href
    const urlSplit = currentUrl.split('#page')
    const cleanPage = urlSplit[0]
    const currentPage = urlSplit.length > 1 ? Number(urlSplit[1]) : 0
    const increment = isDoublePage ? 2 : 1
    const lastPage = pageCount-1
    const nextPage = currentPage == lastPage ? pageCount : Math.min(currentPage+increment, lastPage)
    const prevPage = Math.max(currentPage-increment, 0)
    if(event.keyCode == 37) {
      if (nextPage < pageCount) {
        document.location.href = cleanPage+'#page'+nextPage
      } else {
        document.location.href = nextChapterUrl
      }
    }
    else if(event.keyCode == 39) {
      document.location.href = cleanPage+'#page'+prevPage
    }
  });
}

function setPageGridStyle(doublePageWidth) {
  const bigContainer = getBigContainer()
  const pagesContainer = getPagesContainer()
  bigContainer.classList.add('reader-container')
  bigContainer.style.maxWidth = `${doublePageWidth}px`
  pagesContainer.classList.add('pages-container')
  pagesContainer.parentNode.style.padding = '0'
  function addStyles() {
    var styleSheet = document.createElement('style')
    styleSheet.textContent = styles
    document.head.appendChild(styleSheet)
  }
}

async function getLoadedImages() {
  const imageArray = [].slice.call(getPagesContainer().getElementsByTagName('img'))
  const promiseArray = [];
  for (let image of imageArray) {
    if (!image.complete) {
      promiseArray.push(new Promise(resolve => {
        image.addEventListener('load', () => resolve())
      }));
    }
  }

  await Promise.all(promiseArray); // wait for all the images to be loaded
  return imageArray;
}

function getDoublePageWidth(commonPageWidth) {
  return commonPageWidth*2 + 6
}

async function enhancedReader() {
  console.log('Setting up enhanced reader...')

  const images = await getLoadedImages()

  const chapterButtons = document.getElementsByClassName('btn-capitulo')
  const nextChapterUrl = chapterButtons[chapterButtons.length-1].href

  wrapImages(images)
  const commonPageWidth = getCommonPageWidth(images)
  const doublePageWidth = getDoublePageWidth(commonPageWidth)
  const realPages = addPagination(images, commonPageWidth, nextChapterUrl)



  setPageGridStyle(doublePageWidth)

  // Scroll to current reading page
  const currentUrl = window.location.href
  if (currentUrl.search('#page') ==-1) {
    document.location.href = document.location.href+'#page0'
  } else {
    document.location.href = document.location.href
  }

  isDoublePage = getBigContainer().getBoundingClientRect().width == doublePageWidth

  addKeyboardNavigation(realPages, nextChapterUrl)
  console.log('Finished setting up enhanced reader. Enjoy ;)')
}

async function tryEnhancedReader() {
  try {
    enhancedReader()
  } catch (error) {
    console.error('Something went wrong when loading the enhanced reader', error)
    await sleep(2000);
    tryEnhancedReader()
  }
}

function extractUrlData(url) {
  let match
  if (!url) {
    return {}
  }
  if (url.includes('capitulo')) {
    match = /capitulo\/(\d+)\/(\w+)/.exec(url)
    return {
      'chapter': match[1],
      'mangaId': match[2]
    }
  } else if (url.includes('/manga/')) {
    match = /manga\/([^\/]+)\/(\w+)/.exec(url)
    return {
      'mangaName': match[1],
      'mangaId': match[2]
    }
  }

}

function storeReadingStatus(currentUrl) {
  let readingStatus = GM_getValue(reading_status)
  if (!readingStatus) {
    readingStatus = {}
  }
  const currentData = extractUrlData(currentUrl)
  const mangaId = currentData.mangaId
  const currentChapter = currentData.chapter

  if (currentChapter && readingStatus[mangaId]?.chapter != currentChapter) {
    readingStatus[mangaId] = {
      'chapter': currentChapter ? currentChapter : 1,
      'updated_at': Date.now()
    }
    GM_setValue(reading_status, readingStatus)
  }
}

async function fetchKey(key) {
  const url = `${remoteStorageHost}/GetValue/${getSyncKey()}/${key}`
  return await GM.xmlHttpRequest({ url })
}

async function uploadKey(key, value) {
  const url = `${remoteStorageHost}/UpdateValue/${getSyncKey()}/${key}/${value}`
  return await GM.xmlHttpRequest({ url,  method: 'POST'})
}

function mangaStatusKey(mangaId) {
  return `${remoteKeyPrefix}${mangaId}`
}

async function fetchReadingStatus(mangaId) {
  console.log("fetchReadingStatus ", mangaId)
  return await fetchKey(mangaStatusKey(mangaId))
}

async function uploadReadingStatus(mangaId) {
  const status = btoa(JSON.stringify(GM_getValue(reading_status)[mangaId]))
  console.log("uploadReadingStatus ", mangaId, status)
  return await uploadKey(mangaStatusKey(mangaId), status)
}

async function syncReadingStatus(mangaId) {
  const response = await fetchReadingStatus(mangaId)
  if (response.status == 200) {
    const responseText = JSON.parse(response.responseText)
    if (responseText == '') {
      console.log('Upload status for the first time')
    } else {
      const remoteStatus = JSON.parse(atob(responseText))
      const localStatus = GM_getValue(reading_status)
      const localMangaStatus = localStatus[mangaId]
      if (localMangaStatus && localMangaStatus.updated_at > remoteStatus.updated_at) {
        console.log('Upload status because is newer')
        await uploadReadingStatus(mangaId)
      } else if (!localMangaStatus || localMangaStatus.updated_at < remoteStatus.updated_at) {
        console.log('Store remote status in local')
        localStatus[mangaId] = remoteStatus
        GM_setValue(reading_status, localStatus)
        if (window.location.href.includes('/manga/')) {
          enhancedMangaStatus()
        } else if(window.location.href.includes('/capitulo/') && localStatus[mangaId].chapter != currentChapter) {
          window.location.href = getChapterUrl(mangaId, localStatus[mangaId].chapter)
        }
      }
    }
  }
}

function enhancedMangaStatus() {
  console.log('Setting up enhanced manga status...')
  const currentData = extractUrlData(window.location.href)
  const mangaId = currentData.mangaId
  const readingStatus = GM_getValue(reading_status)[mangaId]
  const currentChapterNumber = Number(readingStatus.chapter)
  const currentChapterUrl = getChapterUrl(mangaId, currentChapterNumber)

  const chaptersContainer = document.getElementsByClassName('capitulos-manga-page')[0]
  const header = chaptersContainer.getElementsByClassName('titulo-ficha-cap')[0]
  let continueButton = document.getElementById('continue-reading')
  if (!continueButton) {
    const wrapper = document.createElement('div')
    wrapper.classList.add('chapter-header')
    continueButton = document.createElement('a')
    continueButton.id = 'continue-reading'

    chaptersContainer.replaceChild(wrapper, header)
    wrapper.appendChild(header)
    wrapper.appendChild(continueButton)
  }
  continueButton.textContent = `Continue chapter ${currentChapterNumber}`
  continueButton.href = currentChapterUrl

  const chapters = chaptersContainer.getElementsByClassName('scroll-list')[0].getElementsByTagName('a')
  for (let i=0;i < chapters.length;i++) {
    const chapterAnchor = chapters[i]
    const chapterData = extractUrlData(chapterAnchor.href)
    if (chapterData.chapter < currentChapterNumber) {
      chapterAnchor.classList.add('disabled')
    } else {
      chapterAnchor.classList.remove('disabled')
    }
  }
  console.log('Done setting up enhanced manga status...')
}

async function tryEnhancedMangaStatus() {
  try {
    enhancedMangaStatus()
  } catch (error) {
    console.error('Something went wrong when loading the enhanced manga status', error)
    await sleep(2000);
    tryEnhancedMangaStatus()
  }
}

let loaded = false

function enhancedReaderObserver() {
  addStyles()
  // Observe for chapter changes and reload to rebuild reader
  const targetNode = document.getElementsByTagName('body')[0]
  const config = { attributes: false, childList: true }
  const callback = (mutationList, observer) => {
    let currentUrl=window.location.href.split('#')[0]
    currentData = extractUrlData(currentUrl)
    mangaId = currentData?.mangaId
    currentChapter = currentData?.chapter
    const lastUrl = GM_getValue(last_url)
    if (currentUrl != lastUrl) {
      loaded = false
      storeReadingStatus(currentUrl)
      GM_setValue(last_url, currentUrl)
    }
    if (!loaded) {
      addNavUtils()
      hideAds()
      if (mangaId) {
        syncReadingStatus(mangaId)
      }
      if (currentUrl.includes('capitulo')) {
        tryEnhancedReader()
      } else if (currentUrl.includes('/manga/')) {
        tryEnhancedMangaStatus()
      }
    }
    loaded = true
  }
  new MutationObserver(callback).observe(targetNode, config)
}

window.addEventListener('load', enhancedReaderObserver, false)
window.addEventListener('resize', (e) => {
  debounce(resetImageSizes, 200)()
}, false)