您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Kanji Visualizer with persistent dropdown
// ==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(); } })();