// ==UserScript== // @name MDL Extra v1.1.0 // @namespace https://github.com/yaruee/mdl-extra // @version 1.1.0 // @description Adds extra functions to MyDramaList: quick +/– episode buttons (profile-only), shortcut panel, and floating search window. // @author yaruee // @icon https://github.com/yaruee/mdl-extra/blob/main/images/icon.png?raw=true // @match https://mydramalist.com/* // @updateURL https://raw.githubusercontent.com/yaruee/mdl-extra/main/mdl-extra.user.js // @downloadURL https://raw.githubusercontent.com/yaruee/mdl-extra/main/mdl-extra.user.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; // ========================= // 🔹 Config + Settings Panel // ========================= let username = GM_getValue('username', ''); let shortcuts = GM_getValue('shortcuts', { profile: 'p', dramalist: 'd', lists: 'l', focusSearch: 's' }); function openSettingsPanel() { const overlayPanel = document.createElement('div'); overlayPanel.style.position = 'fixed'; overlayPanel.style.top = 0; overlayPanel.style.left = 0; overlayPanel.style.width = '100%'; overlayPanel.style.height = '100%'; overlayPanel.style.backgroundColor = 'rgba(0,0,0,0.7)'; overlayPanel.style.display = 'flex'; overlayPanel.style.justifyContent = 'center'; overlayPanel.style.alignItems = 'center'; overlayPanel.style.zIndex = 9999; const panel = document.createElement('div'); panel.style.backgroundColor = '#1e1e1e'; panel.style.color = '#f1f1f1'; panel.style.padding = '20px'; panel.style.borderRadius = '12px'; panel.style.width = '350px'; panel.style.fontFamily = 'Arial, sans-serif'; panel.innerHTML = `

MDL Extra Settings











`; overlayPanel.appendChild(panel); document.body.appendChild(overlayPanel); document.getElementById('mdl-extra-save').addEventListener('click', () => { username = document.getElementById('mdl-extra-username').value.trim(); shortcuts.profile = document.getElementById('mdl-extra-profile').value.toLowerCase(); shortcuts.dramalist = document.getElementById('mdl-extra-dramalist').value.toLowerCase(); shortcuts.lists = document.getElementById('mdl-extra-lists').value.toLowerCase(); shortcuts.focusSearch = document.getElementById('mdl-extra-focus').value.toLowerCase(); GM_setValue('username', username); GM_setValue('shortcuts', shortcuts); alert('Settings saved!'); document.body.removeChild(overlayPanel); }); overlayPanel.addEventListener('click', (e) => { if (e.target === overlayPanel) document.body.removeChild(overlayPanel); }); } GM_registerMenuCommand('MDL Extra Settings', openSettingsPanel); // ========================= // 🔹 Quick +/– Episode Buttons (Profile Only) // ========================= if (window.location.pathname.startsWith('/profile/')) { const profileUser = window.location.pathname.split('/')[2]; if (username && profileUser.toLowerCase() === username.toLowerCase()) { const style = document.createElement('style'); style.textContent = ` .episode-quick-buttons { display:inline-flex; gap:6px; margin-left:6px; vertical-align:middle; } .mdl-quick-btn { display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; font-size:13px; font-weight:700; border-radius:8px; cursor:pointer; transition:background .15s ease,transform .05s ease; user-select:none; border:1px solid; } .mdl-quick-btn:active { transform:translateY(1px); } `; document.head.appendChild(style); function isDarkMode() { const toggle = document.querySelector('.btn-dark-mode .btn-success'); if (toggle && toggle.textContent.trim().toUpperCase() === 'ON') return true; return document.body.classList.contains('dark') || document.body.dataset.theme === 'dark'; } function applyThemeStyles() { const dark = isDarkMode(); document.querySelectorAll('.mdl-quick-btn').forEach(btn => { if (dark) { btn.style.background = '#2b2b2b'; btn.style.color = '#f0f0f0'; btn.style.borderColor = '#444'; } else { btn.style.background = '#f5f5f5'; btn.style.color = '#222'; btn.style.borderColor = '#ccc'; } }); } function waitFor(selector, root = document, timeout = 6000) { return new Promise((resolve, reject) => { const found = root.querySelector(selector); if (found) return resolve(found); const obs = new MutationObserver(() => { const el = root.querySelector(selector); if (el) { obs.disconnect(); resolve(el); } }); obs.observe(root, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); reject(new Error('Timeout: ' + selector)); }, timeout); }); } function createButtons() { const wrap = document.createElement('span'); wrap.className = 'episode-quick-buttons'; const btnPlus = document.createElement('button'); btnPlus.className = 'mdl-quick-btn'; btnPlus.type = 'button'; btnPlus.textContent = '+'; const btnMinus = document.createElement('button'); btnMinus.className = 'mdl-quick-btn'; btnMinus.type = 'button'; btnMinus.textContent = '–'; wrap.appendChild(btnPlus); wrap.appendChild(btnMinus); applyThemeStyles(); return { wrap, btnPlus, btnMinus }; } function addButtons(activityEl, editBtn) { if (!/Currently\s*watching/i.test(activityEl.textContent || '')) return; if (activityEl.parentNode.querySelector('.episode-quick-buttons')) return; activityEl.style.display = 'inline-block'; activityEl.style.verticalAlign = 'middle'; const { wrap, btnPlus, btnMinus } = createButtons(); activityEl.parentNode.insertBefore(wrap, activityEl.nextSibling); const updateEpisodes = async (delta) => { editBtn.click(); try { const input = await waitFor('.el-input__inner'); const current = parseInt(input.value || '0', 10) || 0; const next = Math.max(0, current + delta); input.value = next; input.dispatchEvent(new Event('input', { bubbles: true })); const submitBtn = await waitFor('.el-button.btn.btn-success.el-button--primary'); submitBtn.click(); setTimeout(() => { const closeBtn = document.querySelector('.el-dialog__headerbtn'); if (closeBtn) closeBtn.click(); else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, which: 27 })); }, 600); } catch (e) { console.error('[MDL Extra]', e); } }; btnPlus.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); updateEpisodes(1); }); btnMinus.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); updateEpisodes(-1); }); } function processNewNode(node) { if (node.nodeType !== 1) return; const editBtn = node.querySelector?.('.btn.simple.btn-manage-list'); const activityEl = node.querySelector?.('div.activity'); if (editBtn && activityEl) addButtons(activityEl, editBtn); node.querySelectorAll?.('.list-item, .box, tr, li, .card, .clearfix').forEach(row => { const edit = row.querySelector('.btn.simple.btn-manage-list'); const act = row.querySelector('div.activity'); if (edit && act) addButtons(act, edit); }); } document.querySelectorAll('.list-item, .box, tr, li, .card, .clearfix').forEach(row => { const editBtn = row.querySelector('.btn.simple.btn-manage-list'); const activityEl = row.querySelector('div.activity'); if (editBtn && activityEl) addButtons(activityEl, editBtn); }); const mo = new MutationObserver(mutations => { mutations.forEach(m => m.addedNodes.forEach(processNewNode)); applyThemeStyles(); }); mo.observe(document.body, { childList: true, subtree: true }); const themeToggle = document.querySelector('.btn-dark-mode'); if (themeToggle) { const themeObs = new MutationObserver(applyThemeStyles); themeObs.observe(themeToggle, { childList: true, subtree: true }); } window.addEventListener('load', () => setTimeout(applyThemeStyles, 300)); } } // ========================= // 🔹 Floating Search + Keyboard Shortcuts // ========================= const overlaySearch = document.createElement('div'); overlaySearch.style.position = "fixed"; overlaySearch.style.top = "0"; overlaySearch.style.left = "0"; overlaySearch.style.width = "100%"; overlaySearch.style.height = "100%"; overlaySearch.style.background = "rgba(0,0,0,0.6)"; overlaySearch.style.zIndex = "99998"; overlaySearch.style.display = "none"; document.body.appendChild(overlaySearch); const floatDiv = document.createElement('div'); floatDiv.style.position = 'fixed'; floatDiv.style.top = '33%'; floatDiv.style.left = '50%'; floatDiv.style.transform = 'translateX(-50%)'; floatDiv.style.zIndex = '99999'; floatDiv.style.background = '#1e1e1e'; floatDiv.style.padding = '10px'; floatDiv.style.borderRadius = '10px'; floatDiv.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; floatDiv.style.width = '500px'; floatDiv.style.display = 'none'; const input = document.createElement('input'); input.type = "text"; input.placeholder = "Search dramas..."; input.style.width = "100%"; input.style.padding = "10px"; input.style.border = "1px solid #444"; input.style.borderRadius = "6px"; input.style.background = "#2a2a2a"; input.style.color = "#eee"; input.style.fontSize = "16px"; const suggestionBox = document.createElement('div'); suggestionBox.style.position = "absolute"; suggestionBox.style.top = "60px"; suggestionBox.style.left = "0"; suggestionBox.style.width = "100%"; suggestionBox.style.background = "#2a2a2a"; suggestionBox.style.border = "1px solid #444"; suggestionBox.style.borderRadius = "6px"; suggestionBox.style.boxShadow = "0 2px 8px rgba(0,0,0,0.4)"; suggestionBox.style.maxHeight = "300px"; suggestionBox.style.overflowY = "auto"; suggestionBox.style.display = "none"; suggestionBox.style.color = "#ddd"; floatDiv.appendChild(input); floatDiv.appendChild(suggestionBox); document.body.appendChild(floatDiv); async function fetchSuggestions(query) { if (!query.trim()) { suggestionBox.innerHTML = ""; suggestionBox.style.display = "none"; return; } try { const res = await fetch(`https://mydramalist.com/search?q=${encodeURIComponent(query)}&type=drama`); const text = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html"); const items = doc.querySelectorAll(".box .title a"); suggestionBox.innerHTML = ""; let hasResults = false; items.forEach(a => { const title = a.textContent.trim(); if (!title) return; hasResults = true; const div = document.createElement("div"); div.textContent = title; div.style.padding = "8px"; div.style.cursor = "pointer"; div.style.borderBottom = "1px solid #333"; div.addEventListener("mouseover", () => div.style.background = "#3a3a3a"); div.addEventListener("mouseout", () => div.style.background = "#2a2a2a"); div.addEventListener("click", () => window.location.href = a.href); suggestionBox.appendChild(div); }); suggestionBox.style.display = hasResults ? "block" : "none"; } catch (err) { console.error("Suggestion fetch failed", err); } } function openSearch() { overlaySearch.style.display = "block"; floatDiv.style.display = "block"; input.focus(); input.value = ""; suggestionBox.innerHTML = ""; suggestionBox.style.display = "none"; } function closeSearch() { overlaySearch.style.display = "none"; floatDiv.style.display = "none"; suggestionBox.style.display = "none"; input.value = ""; } overlaySearch.addEventListener("click", closeSearch); input.addEventListener("input", () => fetchSuggestions(input.value)); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { const first = suggestionBox.querySelector("div"); if (first) first.click(); else window.location.href = `https://mydramalist.com/search?q=${encodeURIComponent(input.value)}&type=drama`; } if (e.key === "Escape") closeSearch(); }); document.addEventListener('keydown', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; if (!username) return; const key = e.key.toLowerCase(); if (key === shortcuts.profile) { e.preventDefault(); window.location.href = `https://mydramalist.com/profile/${username}`; } else if (key === shortcuts.dramalist) { e.preventDefault(); window.location.href = `https://mydramalist.com/dramalist/${username}`; } else if (key === shortcuts.lists) { e.preventDefault(); window.location.href = `https://mydramalist.com/profile/${username}/lists`; } else if (key === shortcuts.focusSearch) { e.preventDefault(); openSearch(); } }); })();