// ==UserScript== // @name Lucida Downloader // @description Download music from Spotify, Qobuz, Tidal, Soundcloud, Deezer, Amazon Music and Yandex Music via Lucida. Adds download buttons and floating button. // @icon https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/lucida/lucida.png // @version 1.8 // @author afkarxyz // @namespace https://github.com/afkarxyz/misc-scripts/ // @supportURL https://github.com/afkarxyz/misc-scripts/issues // @license MIT // @match https://open.spotify.com/* // @match https://listen.tidal.com/* // @match https://music.yandex.com/* // @match https://music.amazon.com/* // @match https://www.deezer.com/* // @match https://soundcloud.com/* // @match https://www.qobuz.com/* // @match https://lucida.to/* // @match https://lucida.su/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @downloadURL none // ==/UserScript== (function() { 'use strict'; const DOMAINS = ['lucida.to', 'lucida.su']; const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/lucida/'; const LOGO_SVG = ``; const SERVICES = { '': { name: 'Disabled', icon: '' }, 'spotify': { name: 'Spotify', icon: `${BASE_URL}spotify.png` }, 'qobuz': { name: 'Qobuz', icon: `${BASE_URL}qobuz.png` }, 'tidal': { name: 'Tidal', icon: `${BASE_URL}tidal.svg` }, 'soundcloud': { name: 'Soundcloud', icon: `${BASE_URL}soundcloud.ico` }, 'deezer': { name: 'Deezer', icon: `${BASE_URL}deezer.ico` }, 'amazon': { name: 'Amazon Music', icon: `${BASE_URL}amazon.png` } }; const UPLOAD_SERVICES = { 'pixeldrain': { name: 'Pixeldrain', icon: `${BASE_URL}pixeldrain.png` }, 'filehaus': { name: 'FileHaus', icon: `${BASE_URL}filehaus.ico` }, 'sendcm': { name: 'Send.CM', icon: `${BASE_URL}sendcm.png` }, 'pillowcase': { name: 'Pillowcase.su', icon: `${BASE_URL}pillowcase.png` }, 'krakenfiles': { name: 'KrakenFiles', icon: `${BASE_URL}krakenfiles.png` }, 'gofile': { name: 'Gofile.io', icon: `${BASE_URL}gofile.png` }, 'buzzheavier': { name: 'Buzzheavier', icon: `${BASE_URL}buzzheavier.ico` }, 'litterbox': { name: 'Litterbox', icon: `${BASE_URL}litterbox.ico` } }; GM_addStyle(` .lucida-modal *, .lucida-modal *::before, .lucida-modal *::after { all: initial; box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; font-weight: normal !important; font-size: 14px !important; color: #333; } .lucida-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10000; font-weight: normal; } .lucida-modal { background: #fff; padding: 20px; border-radius: 8px; width: 400px; max-width: 90%; color: #333; font-size: 14px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); font-weight: normal !important; } .lucida-modal h2 { margin: 0 0 20px; color: #f42e8d; font-size: 18px !important; font-weight: 600 !important; line-height: 1.4; } .lucida-modal .preference-group { margin-bottom: 20px; color: #333; } .lucida-modal label { display: block; margin-top: 20px; margin-bottom: 8px; font-weight: 600 !important; font-size: 14px !important; color: #333; } .lucida-modal .header { display: flex; align-items: center; justify-content: flex-start; } .lucida-modal .header img { width: 64px; height: 64px; object-fit: contain; } .lucida-modal .header h2 { margin: 0; } .lucida-modal .preference-group label:first-child { margin-top: 0; } .lucida-modal select { -webkit-appearance: none; -moz-appearance: none; appearance: none; width: 100%; padding: 8px 32px 8px 12px; border: 1px solid #ddd; border-radius: 4px; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") calc(100% - 12px) center no-repeat; cursor: pointer; font-size: 14px !important; color: #333; } .lucida-modal select:hover { border-color: #f42e8d; } .lucida-modal select:focus { outline: none; border-color: #f42e8d; box-shadow: 0 0 0 2px rgba(244, 46, 141, 0.2); } .custom-options { scrollbar-width: thin; scrollbar-color: #f42e8d #f0f0f0; font-size: 14px !important; } .custom-options::-webkit-scrollbar { width: 8px; } .custom-options::-webkit-scrollbar-track { background: #f0f0f0; border-radius: 4px; } .custom-options::-webkit-scrollbar-thumb { background: #f42e8d; border-radius: 4px; } .custom-options::-webkit-scrollbar-thumb:hover { background: #d41d7a; } .service-select-wrapper { position: relative; margin-bottom: 15px; } .custom-select { width: 100%; padding: 8px 32px 8px 12px; border: 1px solid #ddd; border-radius: 4px; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") calc(100% - 12px) center no-repeat; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: all 0.2s ease; user-select: none; font-size: 14px !important; color: #333; } .custom-select span { font-size: 14px !important; color: #333; } .custom-select:hover { border-color: #f42e8d; } .custom-options { position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; border-radius: 4px; margin-top: 4px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .custom-options.show { display: block; } .service-option { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; transition: background-color 0.2s ease; font-weight: normal !important; font-size: 14px !important; color: #333; } .service-option span { font-size: 14px !important; color: #333; } .service-option:hover { background-color: #f5f5f5; } .service-option img, .custom-select img { width: 16px; height: 16px; object-fit: contain; } .lucida-modal .buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .lucida-modal button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: all 0.2s ease; font-size: 14px !important; } .lucida-modal .save-btn { background: linear-gradient(135deg, #f42e8d, #b91c68); color: white; } .lucida-modal .save-btn:hover { background: linear-gradient(135deg, #ff3d9c, #d02077); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(244, 46, 141, 0.4); } .lucida-modal .save-btn:active { transform: translateY(0); box-shadow: 0 1px 4px rgba(244, 46, 141, 0.4); } .lucida-modal .cancel-btn { background: #eee; color: #333; } .lucida-modal .cancel-btn:hover { background: #ddd; color: #333; transform: translateY(-1px); } .lucida-modal .cancel-btn:active { transform: translateY(0); } .floating-button { position: fixed; width: 80px; height: 80px; background-color: transparent; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: move; z-index: 9999; opacity: 0.3; transition: opacity 0.3s ease; border: none; } .floating-button:hover { opacity: 1; } .floating-button svg { width: 48px; height: auto; cursor: pointer; } [role='grid'] { margin-left: 50px; } [data-testid="tracklist-row"] { position: relative; } [role="presentation"] > * { contain: unset; } .btn { width: 40px; height: 40px; border-radius: 50%; border: 0; position: relative; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f42e8d, #b91c68); } .btn:hover { transform: scale(1.1); box-shadow: 0 4px 8px rgba(0,0,0,0.3); } .btn .icon { width: 50%; height: 50%; background-position: center; background-repeat: no-repeat; background-size: contain; background-image: url('data:image/svg+xml;utf8,'); } [data-testid="tracklist-row"] .btn { position: absolute; top: 50%; right: 100%; margin-top: -20px; margin-right: 10px; } .N7GZp8IuWPJvCPz_7dOg .btn { width: 24px; height: 24px; transform-origin: center; position: absolute; top: 50%; right: 100%; margin-top: -12px !important; margin-right: 10px; } .N7GZp8IuWPJvCPz_7dOg .btn .icon { transform: scale(0.85); width: 65%; height: 65%; } `); function createServiceOption(value, service) { const option = document.createElement('div'); option.className = 'service-option'; option.dataset.value = value; if (service.icon) { const img = document.createElement('img'); img.src = service.icon; img.alt = service.name; img.style.display = 'none'; img.onload = () => { img.style.display = 'inline'; }; option.appendChild(img); } const span = document.createElement('span'); span.textContent = service.name; option.appendChild(span); return option; } function updateCustomSelect(customSelect, value, servicesObj = SERVICES) { const service = servicesObj[value]; let content = `${service.name}`; if (service.icon) { const img = new Image(); img.src = service.icon; img.style.display = 'none'; img.onload = () => { img.style.display = 'inline'; customSelect.querySelector('img')?.style.setProperty('display', 'inline'); }; content = `${service.name}${service.name}`; } customSelect.innerHTML = content; } function createPreferencesModal() { const existingModal = document.querySelector('.lucida-modal-overlay'); if (existingModal) { existingModal.remove(); } const modalHTML = `

Lucida Preferences

Select a service
`; const modalContainer = document.createElement('div'); modalContainer.innerHTML = modalHTML; document.body.appendChild(modalContainer.firstElementChild); const customSelect = document.getElementById('custom-service-select'); const customOptions = document.querySelector('.custom-options'); const serviceSelect = document.getElementById('service-select'); const domainSelect = document.getElementById('domain-select'); const floatSelect = document.getElementById('float-select'); const formatSelect = document.getElementById('format-select'); const qualityContainer = document.getElementById('quality-settings-container'); const qualitySelect = document.getElementById('quality-select'); const autoDownloadSelect = document.getElementById('auto-download-select'); const uploadFileSelect = document.getElementById('upload-file-select'); const uploadServiceContainer = document.getElementById('upload-service-container'); const customUploadSelect = document.getElementById('custom-upload-service-select'); const uploadServiceOptions = document.getElementById('upload-service-options'); const uploadServiceSelect = document.getElementById('upload-service-select'); const lucidaIcon = document.querySelector('.lucida-icon'); lucidaIcon.onload = () => { lucidaIcon.style.display = 'inline'; }; lucidaIcon.onerror = () => { lucidaIcon.style.display = 'none'; }; lucidaIcon.addEventListener('click', () => { const domainPref = GM_getValue('domainPreference', 'random'); let domain = domainPref === 'random' ? DOMAINS[Math.floor(Math.random() * DOMAINS.length)] : domainPref; window.open(`https://${domain}/stats`, '_blank'); }); if (domainSelect) domainSelect.value = GM_getValue('domainPreference', 'random'); if (floatSelect) floatSelect.value = GM_getValue('floatIconEnabled', 'enabled'); if (formatSelect) formatSelect.value = GM_getValue('formatPreference', 'original'); if (autoDownloadSelect) autoDownloadSelect.value = GM_getValue('autoDownloadEnabled', 'enabled'); if (uploadFileSelect) uploadFileSelect.value = GM_getValue('uploadFileEnabled', 'disabled'); if (uploadServiceSelect) { const savedUploadService = GM_getValue('uploadServicePreference', 'pixeldrain'); if (savedUploadService && UPLOAD_SERVICES[savedUploadService]) { updateCustomSelect(customUploadSelect, savedUploadService, UPLOAD_SERVICES); uploadServiceSelect.value = savedUploadService; } } const savedService = GM_getValue('targetService', ''); if (savedService && SERVICES[savedService]) { updateCustomSelect(customSelect, savedService); serviceSelect.value = savedService; } function updateQualityOptions(format) { qualitySelect.innerHTML = ''; switch(format) { case 'flac': qualitySelect.innerHTML = ''; qualityContainer.style.display = 'block'; break; case 'mp3': case 'ogg-vorbis': case 'm4a-aac': qualitySelect.innerHTML = ` `; qualityContainer.style.display = 'block'; break; case 'opus': qualitySelect.innerHTML = ` `; qualityContainer.style.display = 'block'; break; default: qualityContainer.style.display = 'none'; } } updateQualityOptions(formatSelect.value); if (qualitySelect) { qualitySelect.value = GM_getValue('qualityPreference', '320'); } formatSelect.addEventListener('change', () => { updateQualityOptions(formatSelect.value); }); uploadFileSelect.addEventListener('change', () => { uploadServiceContainer.style.display = uploadFileSelect.value === 'enabled' ? 'block' : 'none'; }); uploadServiceContainer.style.display = uploadFileSelect.value === 'enabled' ? 'block' : 'none'; Object.entries(SERVICES).forEach(([value, service]) => { const option = createServiceOption(value, service); customOptions.appendChild(option); option.addEventListener('click', () => { serviceSelect.value = value; updateCustomSelect(customSelect, value); customOptions.classList.remove('show'); }); }); Object.entries(UPLOAD_SERVICES).forEach(([value, service]) => { const option = createServiceOption(value, service); uploadServiceOptions.appendChild(option); option.addEventListener('click', () => { uploadServiceSelect.value = value; updateCustomSelect(customUploadSelect, value, UPLOAD_SERVICES); uploadServiceOptions.classList.remove('show'); }); }); customSelect.addEventListener('click', () => { customOptions.classList.toggle('show'); }); customUploadSelect.addEventListener('click', () => { uploadServiceOptions.classList.toggle('show'); }); document.addEventListener('click', (e) => { if (!e.target.closest('.service-select-wrapper')) { customOptions.classList.remove('show'); uploadServiceOptions.classList.remove('show'); } }); const saveBtn = document.querySelector('.save-btn'); if (saveBtn) { saveBtn.addEventListener('click', () => { if (domainSelect && serviceSelect && floatSelect && formatSelect && qualitySelect && autoDownloadSelect && uploadFileSelect && uploadServiceSelect) { GM_setValue('domainPreference', domainSelect.value); GM_setValue('targetService', serviceSelect.value); GM_setValue('floatIconEnabled', floatSelect.value); GM_setValue('formatPreference', formatSelect.value); GM_setValue('qualityPreference', qualitySelect.value); GM_setValue('autoDownloadEnabled', autoDownloadSelect.value); GM_setValue('uploadFileEnabled', uploadFileSelect.value); GM_setValue('uploadServicePreference', uploadServiceSelect.value); const floatingButton = document.querySelector('.floating-button'); if (floatingButton) { floatingButton.style.display = floatSelect.value === 'enabled' ? 'flex' : 'none'; } } document.querySelector('.lucida-modal-overlay').remove(); }); } const cancelBtn = document.querySelector('.cancel-btn'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { document.querySelector('.lucida-modal-overlay').remove(); }); } const modalOverlay = document.querySelector('.lucida-modal-overlay'); if (modalOverlay) { modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { modalOverlay.remove(); } }); } } function autoUploadFile() { if (!window.location.hostname.includes('lucida.')) return Promise.resolve(false); const uploadEnabled = GM_getValue('uploadFileEnabled', 'disabled') === 'enabled'; return new Promise((resolve) => { const handleUploadSettings = () => { const checkbox = document.getElementById('external-enable'); if (checkbox) { checkbox.checked = uploadEnabled; if (!uploadEnabled) { checkbox.dispatchEvent(new Event('change')); return true; } if (uploadEnabled) { checkbox.dispatchEvent(new Event('change')); return selectUploadService(); } } return false; }; const selectUploadService = () => { if (!uploadEnabled) return true; const selects = document.querySelectorAll('select'); const targetService = GM_getValue('uploadServicePreference', 'pixeldrain'); let serviceSelected = false; for (let select of selects) { if (Array.from(select.options).some(opt => opt.value.toLowerCase().includes(targetService))) { select.value = targetService; select.dispatchEvent(new Event('change')); serviceSelected = true; } } return serviceSelected; }; if (!uploadEnabled) { const checkbox = document.getElementById('external-enable'); if (checkbox) { checkbox.checked = false; checkbox.dispatchEvent(new Event('change')); resolve(true); return; } } if (handleUploadSettings()) { resolve(true); return; } const observer = new MutationObserver((mutations, obs) => { if (handleUploadSettings()) { obs.disconnect(); resolve(true); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['value'] }); }); } function autoSelectFormat() { if (!window.location.hostname.includes('lucida.')) return Promise.resolve(false); return new Promise((resolve) => { const selectFormatAndQuality = () => { const convertSelect = document.getElementById('convert'); if (!convertSelect) return false; const format = GM_getValue('formatPreference', 'original'); const quality = GM_getValue('qualityPreference', '320'); convertSelect.value = format; convertSelect.dispatchEvent(new Event('change', { bubbles: true })); const downsettingSelect = document.getElementById('downsetting'); if (downsettingSelect) { downsettingSelect.value = quality; downsettingSelect.dispatchEvent(new Event('change', { bubbles: true })); return true; } return false; }; if (selectFormatAndQuality()) { resolve(true); return; } const observer = new MutationObserver((mutations, obs) => { if (selectFormatAndQuality()) { obs.disconnect(); resolve(true); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['value'] }); }); } function autoDownload() { if (!window.location.hostname.includes('lucida.')) return Promise.resolve(false); if (GM_getValue('autoDownloadEnabled', 'enabled') !== 'enabled') return Promise.resolve(false); return new Promise((resolve) => { const clickDownloadButton = () => { const button = document.querySelector('.d1-track button') || document.querySelector('button[class*="download-button"]'); if (button && button.offsetParent !== null) { button.click(); return true; } return false; }; if (clickDownloadButton()) { resolve(true); return; } const observer = new MutationObserver((mutations, obs) => { if (clickDownloadButton()) { obs.disconnect(); resolve(true); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); }); } async function initializeAutoFunctions() { if (!window.location.hostname.includes('lucida.')) return; try { const uploadEnabled = GM_getValue('uploadFileEnabled', 'disabled') === 'enabled'; await autoSelectFormat(); if (uploadEnabled) { await autoUploadFile(); } else { const checkbox = document.getElementById('external-enable'); if (checkbox) { checkbox.checked = false; checkbox.dispatchEvent(new Event('change')); } } await autoDownload(); } catch (error) { console.error('Error in auto functions:', error); } } function setupMenuCommand() { try { GM_registerMenuCommand('Lucida Preferences', () => { console.log('Opening preferences modal...'); createPreferencesModal(); }); } catch (error) { console.error('Error registering menu command:', error); } } function openInLucida(trackUrl) { const currentUrl = encodeURIComponent(trackUrl || window.location.href); const prefs = getPreferences(); let domain = prefs.domainPreference === 'random' ? DOMAINS[Math.floor(Math.random() * DOMAINS.length)] : prefs.domainPreference; let url = `https://${domain}/?url=${currentUrl}&country=auto`; if (prefs.targetService) { url += `&to=${prefs.targetService}`; } window.open(url, '_blank'); } const getPreferences = () => ({ targetService: GM_getValue('targetService', ''), domainPreference: GM_getValue('domainPreference', 'random') }); function addButton(el) { const button = document.createElement('button'); button.className = 'btn'; const icon = document.createElement('div'); icon.className = 'icon'; button.appendChild(icon); el.appendChild(button); return button; } function addNowPlayingButton() { const downloadButton = document.createElement('button'); downloadButton.className = 'Lucida-Button-sc-1dqy6lx-0 dmdXQN'; downloadButton.innerHTML = ''; downloadButton.style.cssText = 'background:transparent;border:none;color:#f42e8d;cursor:pointer;padding:8px;margin:0 4px;transition:transform .2s ease'; downloadButton.onmouseover = () => downloadButton.style.transform = 'scale(1.1)'; downloadButton.onmouseout = () => downloadButton.style.transform = 'scale(1)'; downloadButton.onclick = () => { const link = document.querySelector('a[href*="spotify:track:"]'); if (link) { const match = link.getAttribute('href').match(/spotify:track:([a-zA-Z0-9]+)/); if (match) { const trackUrl = `https://open.spotify.com/track/${match[1]}`; openInLucida(trackUrl); } } }; const container = document.querySelector('.snFK6_ei0caqvFI6As9Q')?.querySelector('.deomraqfhIAoSB3SgXpu'); if (container && !container.querySelector('.Lucida-Button-sc-1dqy6lx-0')) { container.appendChild(downloadButton); } } function animate() { const currentUrl = window.location.href; const urlParts = currentUrl.split('/'); const type = urlParts[3]; addNowPlayingButton(); if (type === 'track') { const actionBarRow = document.querySelector('.eSg4ntPU2KQLfpLGXAww[data-testid="action-bar-row"]'); if (actionBarRow && !actionBarRow.hasButtons) { const downloadButton = addButton(actionBarRow); downloadButton.onclick = function() { const spotifyId = urlParts[4].split('?')[0]; openInLucida(`https://open.spotify.com/track/${spotifyId}`); } actionBarRow.hasButtons = true; } } if (type === 'artist') { const tracks = document.querySelectorAll('[role="gridcell"]'); tracks.forEach(track => { if (!track.hasButtons) { const downloadButton = addButton(track); downloadButton.onclick = function() { const btn = track.querySelector('[data-testid="more-button"]'); if (btn) { btn.click(); setTimeout(() => { const highlightEl = document.querySelector('#context-menu a[href*="highlight"]'); if (highlightEl) { const highlight = highlightEl.href.match(/highlight=(.+)/)[1]; document.dispatchEvent(new MouseEvent('mousedown')); const spotifyId = highlight.split(':')[2]; openInLucida(`https://open.spotify.com/track/${spotifyId}`); } }, 1); } } track.hasButtons = true; } }); } if (type === 'album' || type === 'playlist' || type === 'track') { const tracks = document.querySelectorAll('[data-testid="tracklist-row"]'); tracks.forEach(track => { if (!track.hasButtons) { const downloadButton = addButton(track); downloadButton.onclick = function() { const trackLink = track.querySelector('a[href^="/track"]'); if (trackLink) { openInLucida(trackLink.href); } else { const btn = track.querySelector('[data-testid="more-button"]'); if (btn) { btn.click(); setTimeout(() => { const highlightEl = document.querySelector('#context-menu a[href*="highlight"]'); if (highlightEl) { const highlight = highlightEl.href.match(/highlight=(.+)/)[1]; document.dispatchEvent(new MouseEvent('mousedown')); const spotifyId = highlight.split(':')[2]; openInLucida(`https://open.spotify.com/track/${spotifyId}`); } }, 1); } } } track.hasButtons = true; } }); } } function animateLoop() { if (window.location.hostname === 'open.spotify.com') { animate(); } requestAnimationFrame(animateLoop); } const button = document.createElement('button'); button.className = 'floating-button'; button.innerHTML = LOGO_SVG; const savedPosition = { left: GM_getValue('buttonLeft', '20'), top: GM_getValue('buttonTop', '20') }; button.style.left = savedPosition.left + 'px'; button.style.top = savedPosition.top + 'px'; let isDragging = false; let startX, startY; button.addEventListener('mousedown', e => { if (e.target.tagName.toLowerCase() !== 'svg') { isDragging = true; startX = e.clientX - button.offsetLeft; startY = e.clientY - button.offsetTop; } }); document.addEventListener('mousemove', e => { if (!isDragging) return; let left = e.clientX - startX; let top = e.clientY - startY; left = Math.max(0, Math.min(window.innerWidth - button.offsetWidth, left)); top = Math.max(0, Math.min(window.innerHeight - button.offsetHeight, top)); button.style.left = left + 'px'; button.style.top = top + 'px'; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; const SNAP = 20; const rect = button.getBoundingClientRect(); if (rect.left < SNAP) button.style.left = '0px'; if (rect.top < SNAP) button.style.top = '0px'; if (window.innerWidth - rect.right < SNAP) button.style.left = (window.innerWidth - rect.width) + 'px'; if (window.innerHeight - rect.bottom < SNAP) button.style.top = (window.innerHeight - rect.height) + 'px'; GM_setValue('buttonLeft', button.style.left.replace('px', '')); GM_setValue('buttonTop', button.style.top.replace('px', '')); }); button.addEventListener('click', e => { if (e.target.closest('svg')) { openInLucida(); } }); const isLucidaDomain = window.location.hostname.includes('lucida.'); if (GM_getValue('floatIconEnabled', 'enabled') === 'disabled' || isLucidaDomain) { button.style.display = 'none'; } document.body.appendChild(button); setupMenuCommand(); requestAnimationFrame(animateLoop); initializeAutoFunctions(); })();