Created
September 1, 2025 15:59
-
-
Save SchmidtDavid/d8edd2a64144c59744d9ce3b58c75927 to your computer and use it in GitHub Desktop.
Fixes: Preloads images right away (especially nice on mokuro.moe - the images start loading basically instantly on my computer/web connection) Combine nearby text boxes so that mokuro stops splitting sentences&words in the middle (enables better parsing, copy paste, etc) Consistent font & font size for the japanese text, higher contrast. Vertica…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Better Mokuro | |
| // @version 1.0 | |
| // @description Enhances Mokuro-generated manga HTML files by preloading images, merging nearby text boxes, applying consistent Japanese font and higher contrast, and enabling vertical scrolling. Disable Migaku before running. | |
| // @author David Schmidt | |
| // @match *://*.mokuro.moe/* | |
| // @note Also works on local Mokuro HTML files (file://) when run in the console | |
| // @run-at document-end | |
| // @license CC0-1.0 | |
| // @namespace https://gist.github.com/ | |
| // ==/UserScript== | |
| (() => { | |
| console.log("Adding Better Mokuro activation button..."); | |
| // Remove any existing button | |
| const existingButton = document.getElementById('activateBetterMokuro'); | |
| if (existingButton) existingButton.remove(); | |
| // Create the activation button | |
| const activateButton = document.createElement('button'); | |
| activateButton.id = 'activateBetterMokuro'; | |
| activateButton.innerHTML = '🚀 Activate Better Mokuro'; | |
| activateButton.style.cssText = ` | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 10000; | |
| padding: 20px 30px; | |
| background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| font-size: 18px; | |
| font-weight: bold; | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.3); | |
| transition: all 0.3s ease; | |
| `; | |
| activateButton.onmouseover = () => { | |
| activateButton.style.transform = 'translate(-50%, -50%) scale(1.05)'; | |
| activateButton.style.boxShadow = '0 12px 35px rgba(0,0,0,0.4)'; | |
| }; | |
| activateButton.onmouseout = () => { | |
| activateButton.style.transform = 'translate(-50%, -50%) scale(1)'; | |
| activateButton.style.boxShadow = '0 8px 25px rgba(0,0,0,0.3)'; | |
| }; | |
| // The entire mokuro conversion script runs when button is clicked | |
| activateButton.onclick = () => { | |
| console.log("🚀 Activating Better Mokuro..."); | |
| // Remove the activation button | |
| activateButton.remove(); | |
| // THE ENTIRE MOKURO SCRIPT STARTS HERE | |
| console.log("Converting mokuro and merging p tags within text boxes..."); | |
| // Force single page mode if state exists | |
| if (typeof state !== 'undefined') { | |
| state.r2l = false; | |
| state.singlePageView = true; | |
| state.hasCover = false; | |
| state.pagesDisplayed = 1; | |
| } | |
| // Track loaded pages to prevent reloading | |
| const loadedPages = new Set(); | |
| // Simple function to merge p tags within a text box | |
| function mergePTagsInTextBox(textBox) { | |
| const pTags = textBox.querySelectorAll('p'); | |
| if (pTags.length <= 1) return; // Nothing to merge | |
| // Extract all text content from p tags | |
| const allTexts = Array.from(pTags).map(p => p.textContent.trim()).filter(text => text); | |
| if (allTexts.length === 0) return; | |
| // Merge text content, removing spaces and line breaks | |
| const mergedText = allTexts.join('').replace(/\s+/g, '').replace(/\n+/g, ''); | |
| // Remove all existing p tags | |
| pTags.forEach(p => p.remove()); | |
| // Create single new p tag with merged content | |
| const newP = document.createElement('p'); | |
| newP.textContent = mergedText; | |
| textBox.appendChild(newP); | |
| console.log(`📝 Merged ${pTags.length} p tags: "${mergedText}"`); | |
| } | |
| // Collect pages with both images and text boxes | |
| const pages = []; | |
| const seen = new Set(); | |
| document.querySelectorAll('.pageContainer').forEach(container => { | |
| const style = container.getAttribute('style') || ''; | |
| const match = style.match(/background-image:\s*url\(["']?([^"')]+)["']?\)/i); | |
| if (match && match[1]) { | |
| const imageUrl = match[1]; | |
| if (!seen.has(imageUrl)) { | |
| seen.add(imageUrl); | |
| // Get all text boxes in this page container | |
| const textBoxes = Array.from(container.querySelectorAll('.textBox')).map(textBox => { | |
| const computedStyle = window.getComputedStyle(textBox); | |
| const clonedBox = textBox.cloneNode(true); | |
| // Merge p tags within this text box | |
| mergePTagsInTextBox(clonedBox); | |
| return { | |
| element: clonedBox, | |
| left: parseFloat(computedStyle.left) || 0, | |
| top: parseFloat(computedStyle.top) || 0, | |
| width: parseFloat(computedStyle.width) || 0, | |
| height: parseFloat(computedStyle.height) || 0, | |
| originalStyle: textBox.getAttribute('style') || '' | |
| }; | |
| }); | |
| const containerComputedStyle = window.getComputedStyle(container); | |
| pages.push({ | |
| imageUrl, | |
| textBoxes, | |
| containerWidth: parseFloat(containerComputedStyle.width) || 800, | |
| containerHeight: parseFloat(containerComputedStyle.height) || 1200, | |
| originalContainer: container | |
| }); | |
| } | |
| } | |
| }); | |
| if (pages.length === 0) { | |
| console.error("No pages found!"); | |
| return; | |
| } | |
| console.log(`Found ${pages.length} pages`); | |
| // Hide existing containers | |
| const pagesContainer = document.getElementById('pagesContainer'); | |
| if (pagesContainer) { | |
| pagesContainer.style.display = 'none'; | |
| } | |
| // Create vertical container | |
| const verticalContainer = document.createElement('div'); | |
| verticalContainer.id = 'verticalMangaContainer'; | |
| verticalContainer.style.cssText = ` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| background: #000; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 0; | |
| box-sizing: border-box; | |
| scroll-behavior: smooth; | |
| `; | |
| // Add CSS with universal font sizing | |
| const styleSheet = document.createElement('style'); | |
| styleSheet.textContent = ` | |
| #verticalMangaContainer .textBox { | |
| position: absolute; | |
| pointer-events: auto; | |
| cursor: text; | |
| user-select: text; | |
| z-index: 10; | |
| border-radius: 0.25em; | |
| } | |
| #verticalMangaContainer .textBox p { | |
| opacity: 1; | |
| background: rgba(255, 255, 255, 0.95); | |
| color: rgb(0, 0, 0); | |
| margin: 0; | |
| padding: 0.25em; | |
| border-radius: 0.25em; | |
| border: 0.125em solid rgba(0, 0, 0, 0.2); | |
| line-height: 1.3; | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| font-family: 'Noto Sans CJK JP', 'Yu Gothic', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif; | |
| font-size: 18px !important; | |
| font-weight: 400; | |
| transition: all 0.2s ease; | |
| } | |
| #verticalMangaContainer .textBox:hover p { | |
| background: rgba(255, 255, 255, 1); | |
| color: rgb(0, 0, 0); | |
| border: 0.125em solid rgba(0, 0, 0, 0.4); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); | |
| transform: scale(1.02); | |
| } | |
| `; | |
| document.head.appendChild(styleSheet); | |
| // Create pages with images and text overlays | |
| pages.forEach((page, pageIndex) => { | |
| const pageContainer = document.createElement('div'); | |
| pageContainer.style.cssText = ` | |
| position: relative; | |
| width: 100%; | |
| max-width: 100vw; | |
| display: flex; | |
| justify-content: center; | |
| background: #000; | |
| `; | |
| pageContainer.dataset.pageNumber = pageIndex + 1; | |
| const img = document.createElement('img'); | |
| img.src = page.imageUrl; | |
| img.alt = `Page ${pageIndex + 1}`; | |
| img.style.cssText = ` | |
| max-width: 100%; | |
| max-height: 100vh; | |
| width: auto; | |
| height: auto; | |
| display: block; | |
| object-fit: contain; | |
| `; | |
| // Handle image load to position text boxes correctly | |
| img.onload = () => { | |
| const pageKey = `${pageIndex + 1}`; | |
| if (loadedPages.has(pageKey)) { | |
| return; // Already loaded, don't process again | |
| } | |
| loadedPages.add(pageKey); | |
| console.log(`Loaded page ${pageIndex + 1}`); | |
| // Calculate scaling factors | |
| const imgRect = img.getBoundingClientRect(); | |
| const scaleX = imgRect.width / page.containerWidth; | |
| const scaleY = imgRect.height / page.containerHeight; | |
| // Position text boxes relative to the image | |
| page.textBoxes.forEach((textBoxData) => { | |
| const textBox = textBoxData.element; | |
| // Calculate new position relative to the image | |
| const newLeft = textBoxData.left * scaleX; | |
| const newTop = textBoxData.top * scaleY; | |
| const newWidth = textBoxData.width * scaleX; | |
| const newHeight = textBoxData.height * scaleY; | |
| // Position relative to the page container | |
| const pageContainerRect = pageContainer.getBoundingClientRect(); | |
| const imgOffsetX = (pageContainerRect.width - imgRect.width) / 2; | |
| textBox.style.cssText = ` | |
| position: absolute; | |
| left: ${imgOffsetX + newLeft}px; | |
| top: ${newTop}px; | |
| width: ${newWidth}px; | |
| height: ${newHeight}px; | |
| z-index: 10; | |
| `; | |
| // Set universal font size for the p tag | |
| const pTag = textBox.querySelector('p'); | |
| if (pTag) { | |
| const baseFontSize = 18; | |
| const scaledFontSize = baseFontSize * Math.min(scaleX, scaleY); | |
| pTag.style.fontSize = `${scaledFontSize}px !important`; | |
| pTag.style.fontFamily = "'Noto Sans CJK JP', 'Yu Gothic', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif"; | |
| pTag.style.fontWeight = "400"; | |
| } | |
| // Preserve original classes | |
| textBox.className = (textBoxData.element.className + ' textBox').trim(); | |
| pageContainer.appendChild(textBox); | |
| }); | |
| }; | |
| img.onerror = () => { | |
| console.error(`Failed to load page ${pageIndex + 1}`); | |
| }; | |
| pageContainer.appendChild(img); | |
| verticalContainer.appendChild(pageContainer); | |
| }); | |
| // Simple navigation | |
| const closeButton = document.createElement('button'); | |
| closeButton.textContent = '✕ Restore'; | |
| closeButton.style.cssText = ` | |
| position: fixed; | |
| top: 10px; | |
| right: 10px; | |
| z-index: 1001; | |
| padding: 8px 12px; | |
| background: rgba(0,0,0,0.8); | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| `; | |
| closeButton.onclick = () => { | |
| styleSheet.remove(); | |
| verticalContainer.remove(); | |
| closeButton.remove(); | |
| if (pagesContainer) pagesContainer.style.display = ''; | |
| location.reload(); | |
| }; | |
| // Disable existing mokuro behavior | |
| if (typeof pz !== 'undefined' && pz.dispose) { | |
| try { | |
| pz.dispose(); | |
| } catch (e) { | |
| console.log("Could not dispose panzoom:", e); | |
| } | |
| } | |
| const functionsToDisable = [ | |
| 'nextPage', 'prevPage', 'firstPage', 'lastPage', | |
| 'updatePage', 'zoomFitToScreen', 'zoomFitToWidth', 'panAlign' | |
| ]; | |
| functionsToDisable.forEach(funcName => { | |
| if (typeof window[funcName] === 'function') { | |
| window[funcName] = () => console.log(`${funcName} disabled in vertical mode`); | |
| } | |
| }); | |
| // Add keyboard navigation | |
| verticalContainer.addEventListener('keydown', (e) => { | |
| const scrollAmount = window.innerHeight * 0.8; | |
| switch(e.key) { | |
| case 'ArrowDown': | |
| case ' ': | |
| case 'PageDown': | |
| e.preventDefault(); | |
| verticalContainer.scrollBy({ top: scrollAmount, behavior: 'smooth' }); | |
| break; | |
| case 'ArrowUp': | |
| case 'PageUp': | |
| e.preventDefault(); | |
| verticalContainer.scrollBy({ top: -scrollAmount, behavior: 'smooth' }); | |
| break; | |
| case 'Home': | |
| e.preventDefault(); | |
| verticalContainer.scrollTo({ top: 0, behavior: 'smooth' }); | |
| break; | |
| case 'End': | |
| e.preventDefault(); | |
| verticalContainer.scrollTo({ top: verticalContainer.scrollHeight, behavior: 'smooth' }); | |
| break; | |
| case 'Escape': | |
| closeButton.click(); | |
| break; | |
| } | |
| }); | |
| // Add to page | |
| document.body.appendChild(verticalContainer); | |
| document.body.appendChild(closeButton); | |
| // Focus for keyboard navigation | |
| verticalContainer.tabIndex = -1; | |
| verticalContainer.focus(); | |
| console.log("🎯 Done! Merged multiple p tags within text boxes into single p tags"); | |
| // END OF MOKURO SCRIPT | |
| }; | |
| // Add button to page | |
| document.body.appendChild(activateButton); | |
| console.log("✅ Activation button added! Close console, then click the big button to activate Better Mokuro."); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment