Greasy Fork is available in English.

MH - Journal Screenshot Button

Adds a button to the top of journal to generate and save a screenshot of the last N journal entries, optionally excluding non-hunts

// ==UserScript==
// @name         MH - Journal Screenshot Button
// @namespace    https://greasyfork.org/users/918578
// @description  Adds a button to the top of journal to generate and save a screenshot of the last N journal entries, optionally excluding non-hunts
// @author       squash
// @match        https://www.mousehuntgame.com/*
// @match        http://www.mousehuntgame.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=mousehuntgame.com
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/html2canvas@1.4/dist/html2canvas.min.js
// @version      0.3
// ==/UserScript==

// TODO: Is type filtering accurate? Are active passive and linked the only hunt entry types?

;(function () {
	'use strict'

	let css = `
		#journalContainer.journal--privacy .journal__screenshot-preview .entry:not(.badge) a[href*="profile.php"],
		#journalContainer.journal--privacy .journal__screenshot-preview .entry.socialGift .journaltext a,
		#journalContainer.journal--privacy .journal__screenshot-preview .relicHunter_complete > .journalbody > .journaltext > b:nth-child(6),
		#journalContainer.journal--privacy .journal__screenshot-preview .wanted_poster-complete > .journalbody > .journaltext > b:nth-child(8),
		#journalContainer.journal--privacy .journal__screenshot-preview .journal__hunter-name
		{
			visibility: hidden !important;
		}

		#journalContainer .journal__screenshot-button {
			position: absolute;
			left: 10px;
			top: 60%;
			transform: translateY(-60%);
			font-size: 10px;
			line-height: 20px;
			padding: 0 5px 0 5px;
			border-radius: 5px;
			background: #ccc;
			border: 1px solid #333;
			text-decoration: none;
			display: none;
		}

		#journalContainer .journal__screenshot-button:hover {
			text-decoration: none;
			background-color: #eee;
		}

		#journalContainer .top:hover .journal__screenshot-button {
			display: block;
		}

		.journal__screenshot-preview {
			display: grid;
			grid-template-rows: repeat(12, auto);
			grid-gap: 0px;
			grid-auto-flow: column;
			overflow:visible;
		}

		.journal__screenshot-preview .entry {
			width: 349px;
		}
		`

	const styleNode = document.createElement('style')
	styleNode.appendChild(document.createTextNode(css))
	;(document.querySelector('head') || document.documentElement).appendChild(styleNode)

	// Create screenshot button
	const button = document.createElement('a')
	button.innerText = 'Screenshot'
	button.href = '#'
	button.className = 'journal__screenshot-button'
	button.onclick = function (e) {
		e.preventDefault()

		createOptionsDialog()
	}

	function createOptionsDialog() {
		let dialog = new jsDialog()
		dialog.setTemplate('ajax')
		dialog.setIsModal(true)
		dialog.addToken('{*prefix*}', '<h2 class="title">Journal Screenshot</h2>')
		let content = `<form>`
		content += `<p><label>Include <input type="number" min="1" max="72" name="entries" value="5" step="1" /> entries starting from the top of Page ${parseInt(
			document.querySelector('.pagerView-section-currentPage').innerText
		)}. </label></p>`
		content += `<p><label>Exclude non-hunt entries? <input type="checkbox" name="exclude_nonhunts" value="1" checked /></label></p>`
		content += `<p><label>Hide hunter names? <input type="checkbox" name="censor" value="1" checked /></label></p>`
		content += '</form>'
		dialog.addToken('{*content*}', content)
		dialog.addToken('{*suffix*}', `<input class="jsDialogClose" type="button" value="Cancel" /> <input class="jsDialogClose journal__screenshot-save" type="button" value="Save Screenshot" />`)
		dialog.show()

		// Bind save screenshot event
		document.querySelector('#overlayPopup .journal__screenshot-save').addEventListener('click', async function (event) {
			let preview = await createPreviewElement()

			// Temporarily append preview to journal so it's styled correctly
			document.querySelector('#journalContainer .content').append(preview)

			// Wrap unselectable names in case we're censoring them
			findUnselectableNames()

			// Generate screenshot and remove preview again
			html2canvas(preview, { logging: false, width: preview.scrollWidth, height: preview.scrollHeight, backgroundColor: '#f1f1f1' }).then((canvas) => {
				let entryTime = preview.querySelector('.journaldate').innerText.split(' - ')[0].replace(':', '_')
				saveAs(canvas.toDataURL('image/png'), `screenshot-${new Date().toISOString().split('T')[0]}_${entryTime}.png`)
				preview.remove()
				document.querySelector('#journalContainer').classList.remove('journal--privacy')
			})
		})
	}

	async function createPreviewElement() {
		let config = Object.fromEntries(new FormData(document.querySelector('.jsDialog form')).entries())
		config.entries = parseInt(config.entries)
		if (config.entries > 72) {
			config.entries == 72
		}

		let preview = document.createElement('div')
		preview.classList = 'journal__screenshot-preview'

		let elements = document.querySelectorAll(config.exclude_nonhunts ? '#journalContainer .entry.active, #journalContainer .entry.passive, #journalContainer .entry.linked' : '.entry')
		let nextPage = getNextPage()

		let container = document.querySelector('#journalContainer')

		if (config.censor) {
			container.classList.add('journal--privacy')
		} else {
			container.classList.remove('journal--privacy')
		}

		// Fetch next journal page until we have all relevant entries or run out of pages
		while (elements.length < config.entries && nextPage <= 6) {
			console.log('Fetching Journal Page: ' + nextPage)

			let response = await fetch(callbackurl + 'managers/ajax/pages/journal.php', {
				method: 'POST',
				headers: {
					Accept: 'application/json',
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: new URLSearchParams({ page: nextPage, size: container.dataset.size, owner: container.dataset.owner }),
			})
			let data = await response.json()
			let parser = new DOMParser()
			let journalPage = parser.parseFromString(data.journal_page.entries_string, 'text/html')

			elements = [...elements].concat([...journalPage.querySelectorAll(config.exclude_nonhunts ? '.entry.active, .entry.passive, .entry.linked' : '.entry')])

			nextPage++
		}

		// Append relevant number of journal entires to preview
		let i = 0
		for (let element of elements) {
			i++
			if (i > config.entries) {
				break
			}
			preview.appendChild(element.cloneNode(true))
		}

		return preview
	}

	function getNextPage() {
		let page = parseInt(document.querySelector('.pagerView-section-currentPage').innerText)
		return page + 1
	}

	function saveAs(uri, filename) {
		var link = document.createElement('a')

		if (typeof link.download === 'string') {
			link.href = uri
			link.download = filename

			// Firefox requires the link to be in the body
			document.body.appendChild(link)
			link.click()
			document.body.removeChild(link)
		} else {
			window.open(uri)
		}
	}

	function findUnselectableNames() {
		// Some map-related journal entries don't have selector-friendly names, so they need to be found and wrapped.
		const mapEntries = document.querySelectorAll('#journalContainer .entry.relicHunter_start .journaltext')
		if (mapEntries) {
			mapEntries.forEach(function (entry) {
				let index = entry.innerHTML.indexOf(' has joined the ')

				if (index === -1) {
					index = entry.innerHTML.indexOf(' has left the ')
				}
				if (index === -1) {
					index = entry.innerHTML.indexOf(' used Rare Map Dust')
				}
				if (index === -1) {
					index = entry.innerHTML.indexOf(', the map owner, has activated Consolation Mode')
				}
				if (index !== -1) {
					if (!entry.querySelector('.journal__hunter-name')) {
						entry.innerHTML = ['<span class="journal__hunter-name">', entry.innerHTML.slice(0, index), '</span>', entry.innerHTML.slice(index)].join('')
					}
				}
			})
		}
	}

	function renderButton() {
		if (!document.querySelector('.journal__screenshot-button')) {
			const journalTop = document.querySelector('#journalContainer .top')
			if (journalTop) {
				journalTop.prepend(button)
			}
		}
	}

	renderButton()

	if (typeof eventRegistry !== 'undefined') {
		eventRegistry.addEventListener(hg.utils.PageUtil.EventSetPage, function (currentPage) {
			if (currentPage.type == 'PageCamp') {
				renderButton()
			}
		})
	}
})()