// ==UserScript==
// @name Kanji Visualizer
// @namespace https://marumori.io/
// @version 0.1
// @description Kanji Visualizer with persistent dropdown
// @author Matskye
// @match https://marumori.io/*
// @grant GM.xmlHttpRequest
// @connect public-api.marumori.io
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Utility function to log messages with a prefix
function log(message) {
console.log(`[Kanji-Visualizer]: ${message}`);
}
// Function to inject the "Scripts" dropdown into the navbar
function injectScriptsDropdown() {
// Potential navbar selectors based on site inspection
const navbarSelectors = [
'nav',
'[role="navigation"]',
'.navbar',
'.nav',
'[aria-label="main navigation"]',
'[data-testid="navbar"]',
'#main-navbar'
];
let navbar = null;
// Find the navbar element
for (const selector of navbarSelectors) {
navbar = document.querySelector(selector);
if (navbar) {
log(`Navbar found using selector: "${selector}"`);
break;
}
}
if (!navbar) {
log('Navbar not found. Will retry...');
return; // Exit if navbar not found; observer will handle retries
}
// Check if the "Scripts" dropdown is already injected
if (document.getElementById('scripts-dropdown-wrapper')) {
log('"Scripts" dropdown already exists. Skipping injection.');
return;
}
// Create the dropdown wrapper matching existing site structure
const dropdownWrapper = document.createElement('div');
dropdownWrapper.className = 'sub-menu-wrapper svelte-pchasl';
dropdownWrapper.id = 'scripts-dropdown-wrapper'; // Unique ID to prevent duplication
dropdownWrapper.style.position = 'relative'; // Establish positioning context
// Create the profile list container
const profileList = document.createElement('ul');
profileList.className = 'profile-list-wrapper svelte-pchasl';
// Create the "Scripts" button
const scriptsLi = document.createElement('li');
const scriptsButton = document.createElement('button');
scriptsButton.className = 'svelte-1irkqfc';
scriptsButton.setAttribute('aria-haspopup', 'true');
scriptsButton.setAttribute('aria-expanded', 'false');
const scriptsLink = document.createElement('a');
scriptsLink.href = '#'; // Prevent default navigation
scriptsLink.className = 'link svelte-1irkqfc';
scriptsLink.innerHTML = `
<svg class="icon undefined" style="width: 1.5rem; height: 1.5rem;" viewBox="0 0 24 24" fill="var(--dark-gray)" xmlns="http://www.w3.org/2000/svg">
<!-- Replace the path below with the actual SVG path from the site's existing dropdown icons -->
<path d="M7 10l5 5 5-5H7z"></path>
</svg>
<span class="text svelte-1irkqfc">Scripts</span>
`;
scriptsButton.appendChild(scriptsLink);
scriptsLi.appendChild(scriptsButton);
profileList.appendChild(scriptsLi);
// Append the profile list to the dropdown wrapper
dropdownWrapper.appendChild(profileList);
// Create the dropdown content container
const dropdownContent = document.createElement('div');
dropdownContent.className = 'sub-menu-content svelte-pchasl'; // Assuming similar class for dropdown content
dropdownContent.style.position = 'absolute';
dropdownContent.style.top = '100%'; // Position directly below the button
dropdownContent.style.left = '0';
dropdownContent.style.zIndex = '1000'; // Ensure it overlays other content
dropdownContent.style.display = 'none'; // Hidden by default
dropdownContent.style.backgroundColor = 'var(--navbar-background, #808080)'; // Match navbar background
dropdownContent.style.minWidth = '160px'; // Minimum width
dropdownContent.style.boxShadow = '0px 8px 16px 0px rgba(0,0,0,0.2)';
dropdownContent.style.borderRadius = '4px';
dropdownContent.style.padding = '5px 0'; // Optional padding
// Create the list for dropdown items
const dropdownUl = document.createElement('ul');
dropdownUl.className = 'profile-list-wrapper svelte-pchasl';
dropdownUl.style.listStyle = 'none'; // Remove default list styles
dropdownUl.style.margin = '0';
dropdownUl.style.padding = '0';
// Create the "Start Kanji Visualizer" item
const kanjiLi = document.createElement('li');
const kanjiButton = document.createElement('button');
kanjiButton.className = 'svelte-1irkqfc';
kanjiButton.id = 'start-kanji-visualizer';
kanjiButton.setAttribute('aria-label', 'Start Kanji Visualizer');
kanjiButton.style.width = '100%'; // Make button full width
kanjiButton.style.background = 'none';
kanjiButton.style.border = 'none';
kanjiButton.style.padding = '10px 20px';
kanjiButton.style.textAlign = 'left';
kanjiButton.style.cursor = 'pointer';
kanjiButton.style.fontSize = '16px';
const kanjiLink = document.createElement('a');
kanjiLink.href = '#'; // Prevent default navigation
kanjiLink.className = 'link svelte-1irkqfc';
kanjiLink.innerHTML = `
<svg class="icon undefined" style="width: 1.5rem; height: 1.5rem; vertical-align: middle; margin-right: 8px;" viewBox="0 0 24 24" fill="var(--dark-gray)" xmlns="http://www.w3.org/2000/svg">
<!-- Replace the path below with the actual SVG path from the site's existing dropdown icons -->
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"></path>
</svg>
<span class="text svelte-1irkqfc">Start Kanji Visualizer</span>
`;
kanjiButton.appendChild(kanjiLink);
kanjiLi.appendChild(kanjiButton);
dropdownUl.appendChild(kanjiLi);
dropdownContent.appendChild(dropdownUl);
// Append the dropdown content to the wrapper
dropdownWrapper.appendChild(dropdownContent);
// Append the dropdown wrapper to the navbar
navbar.appendChild(dropdownWrapper);
log('"Scripts" dropdown injected successfully.');
// Attach event listeners for accessibility and functionality
scriptsButton.addEventListener('click', function(event) {
event.preventDefault();
const expanded = scriptsButton.getAttribute('aria-expanded') === 'true';
scriptsButton.setAttribute('aria-expanded', !expanded);
dropdownContent.style.display = expanded ? 'none' : 'block';
});
// Close the dropdown when clicking outside
document.addEventListener('click', function(event) {
if (!dropdownWrapper.contains(event.target)) {
scriptsButton.setAttribute('aria-expanded', 'false');
dropdownContent.style.display = 'none';
}
});
// Attach event listener to "Start Kanji Visualizer"
kanjiButton.addEventListener('click', function(event) {
event.preventDefault();
showApiPopup();
});
}
// Function to show the API key popup
function showApiPopup() {
// Create overlay
const overlay = document.createElement('div');
overlay.id = 'popup-overlay';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.zIndex = '1000';
// Create popup container
const popup = document.createElement('div');
popup.id = 'api-popup';
popup.innerHTML = `
<div style="background-color: var(--background-color, #fff); padding: 20px; border-radius: 8px; width: 300px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<h2 style="margin-top: 0; text-align: center;">Enter Your API Key</h2>
<label for="api-key">API Key:</label>
<input type="text" id="api-key" style="width: 100%; padding: 8px; margin: 10px 0; box-sizing: border-box;" />
<button id="submit-api" style="width: 100%; padding: 10px; background-color: var(--primary-color, #007BFF); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Submit</button>
<div id="error-message" style="color: red; margin-top: 10px; text-align: center;"></div>
</div>
`;
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.zIndex = '1001'; // Higher than overlay
// Append overlay and popup to the body
document.body.appendChild(overlay);
document.body.appendChild(popup);
// Handle API key submission
document.getElementById('submit-api').addEventListener('click', function() {
const apiKey = document.getElementById('api-key').value.trim();
if (!apiKey) {
displayError('Please enter your API key.');
return;
}
log('API key submitted.');
overlay.remove();
popup.remove();
fetchKanjiData(apiKey);
});
// Close popup when clicking outside the popup container
overlay.addEventListener('click', function() {
overlay.remove();
popup.remove();
});
function displayError(message) {
const errorDiv = document.getElementById('error-message');
if (errorDiv) {
errorDiv.textContent = message;
}
}
// Reuse the log function from the main script
function log(message) {
console.log(`[Kanji-Visualizer]: ${message}`);
}
}
function fetchKanjiData(apiKey) {
log('Fetching Kanji data with API key...');
GM.xmlHttpRequest({
method: 'GET',
url: 'https://public-api.marumori.io/known/kanji',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
onload: function(response) {
log(`API response status: ${response.status}`);
if (response.status === 200) {
try {
const kanjiData = JSON.parse(response.responseText);
log('Parsed Kanji data successfully.');
if (kanjiData && Array.isArray(kanjiData.items)) {
log(`Kanji data received. Total items: ${kanjiData.items.length}`);
openNewTabWithKanji(kanjiData.items);
} else {
alert('Unexpected response format, no kanji data found.');
log('Response data does not contain "items" array.');
}
} catch (error) {
console.error('Error parsing Kanji data:', error);
alert('Error parsing Kanji data. Check the console for more details.');
}
} else {
alert(`Failed to fetch Kanji data. Status: ${response.status}`);
log('Response text:', response.responseText);
}
},
onerror: function(error) {
console.error('Error fetching Kanji:', error);
alert('Error fetching Kanji. Check the console for more details.');
}
});
}
// Function to open a new tab and display Kanji data as an image
function openNewTabWithKanji(kanjiData) {
log('Opening new tab for Kanji display...');
const newTab = window.open('', '_blank');
if (!newTab) {
alert('Unable to open a new tab. Please enable pop-ups.');
return;
}
// Serialize the Kanji data to pass to the new tab
const kanjiDataJSON = JSON.stringify(kanjiData);
// Write the HTML content to the new tab
newTab.document.open();
newTab.document.write(`
<html>
<head>
<title>Kanji Visualizer</title>
<style>
body {
margin: 0;
padding: 20px;
background-color: #000000; /* Black background */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
color: #FFFFFF;
font-family: Arial, sans-serif;
}
#kanjiCanvas {
border: 2px solid #FFFFFF;
}
#downloadBtn {
margin-top: 20px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #444444;
color: #FFFFFF;
border: none;
border-radius: 5px;
}
#downloadBtn:hover {
background-color: #666666;
}
</style>
</head>
<body>
<canvas id="kanjiCanvas"></canvas>
<button id="downloadBtn">Download Image</button>
<script>
// Parse the Kanji data passed from the parent window
const kanjiData = ${kanjiDataJSON};
// Function to map level to color
function getColor(level) {
level = parseInt(level, 10); // Ensure level is integer
const colors = {
1: '#8B0000', // Dark Red
2: '#FF0000', // Red
3: '#FF4500', // Orange Red
4: '#FFA500', // Orange
5: '#FFD700', // Gold
6: '#008000', // Green
7: '#00CED1', // Dark Turquoise (a shade of blue)
8: '#0000FF', // Blue
9: '#ADD8E6' // Light Blue
};
return colors[level] || '#FFFFFF'; // Default to white
}
// Function to draw Kanji on canvas
function drawKanji() {
const canvas = document.getElementById('kanjiCanvas');
const ctx = canvas.getContext('2d');
// Define canvas dimensions based on number of Kanji
const kanjiPerRow = 30; // Adjust as needed
const kanjiSize = 30; // Font size in pixels
const padding = 10; // Padding between Kanji
const rows = Math.ceil(kanjiData.length / kanjiPerRow);
canvas.width = kanjiPerRow * (kanjiSize + padding);
canvas.height = rows * (kanjiSize + padding);
// Fill background with black
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Set Kanji font
ctx.font = \`\${kanjiSize}px Arial\`;
ctx.textBaseline = 'top';
kanjiData.forEach((item, index) => {
const row = Math.floor(index / kanjiPerRow);
const col = index % kanjiPerRow;
const x = col * (kanjiSize + padding);
const y = row * (kanjiSize + padding);
// Set color based on level
ctx.fillStyle = getColor(item.level);
// Optional: Add shadow for better visibility
ctx.shadowColor = '#000000';
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
// Draw Kanji character
ctx.fillText(item.item, x, y);
});
log('Kanji rendered on canvas.');
}
// Logging function
function log(message) {
console.log(\`[Kanji-Visualizer]: \${message}\`);
}
// Draw Kanji when the page loads
window.onload = drawKanji;
// Handle download button click
document.getElementById('downloadBtn').addEventListener('click', function() {
const canvas = document.getElementById('kanjiCanvas');
const link = document.createElement('a');
link.download = 'kanji_visualizer.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
</script>
</body>
</html>
`);
newTab.document.close();
log('Kanji rendering script injected into new tab.');
}
// Function to observe navbar mutations and inject dropdown when necessary
function observeNavbar() {
// Potential navbar selectors based on site inspection
const navbarSelectors = [
'nav',
'[role="navigation"]',
'.navbar',
'.nav',
'[aria-label="main navigation"]',
'[data-testid="navbar"]',
'#main-navbar' // Adjust if there's a unique ID
];
// Function to find and inject the dropdown
function findAndInject() {
let navbar = null;
for (const selector of navbarSelectors) {
navbar = document.querySelector(selector);
if (navbar) {
log(`Navbar found using selector: "${selector}"`);
break;
}
}
if (navbar) {
injectScriptsDropdown();
} else {
log('Navbar not found during observation.');
}
}
// Create a MutationObserver to watch for changes in the navbar
const observer = new MutationObserver((mutations, obs) => {
for (let mutation of mutations) {
if (mutation.type === 'childList' || mutation.type === 'subtree') {
log('Navbar mutation detected. Checking for dropdown...');
findAndInject();
}
}
});
// Start observing each navbar selector
navbarSelectors.forEach(selector => {
const targetNode = document.querySelector(selector);
if (targetNode) {
observer.observe(targetNode, { childList: true, subtree: true });
log(`Observing changes to navbar using selector: "${selector}"`);
}
});
// Fallback: Observe the entire document for navbar additions
observer.observe(document.body, { childList: true, subtree: true });
log('Started observing the document for navbar changes.');
}
// Initialize the script
function init() {
// Initial injection attempt
injectScriptsDropdown();
// Start observing for dynamic changes
observeNavbar();
}
// Run the initializer after the DOM is fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();