diff --git a/static/js/script.js b/static/js/script.js index 24a0102..7ffe6e2 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -7,79 +7,15 @@ document.addEventListener('DOMContentLoaded', () => { if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; } - - // Прокручиваем в самый верх window.scrollTo(0, 0); // --- Copy TON Address --- const copyTonButton = document.getElementById('copy-ton-button'); const tonAddressElement = document.getElementById('ton-address'); - // --- Helper Function for execCommand Fallback --- - function copyUsingExecCommand(textToCopy) { - let success = false; - // Create a temporary textarea element - const textArea = document.createElement('textarea'); - textArea.value = textToCopy; - - // Make it non-editable to avoid focus issues - textArea.setAttribute('readonly', ''); - - // ** IMPORTANT ** Style to make it invisible but selectable - // Needs to be in the DOM and selectable to work - textArea.style.position = 'absolute'; - textArea.style.left = '-9999px'; // Move off-screen - textArea.style.top = '0'; - textArea.style.opacity = '0'; // Hide visually - textArea.style.pointerEvents = 'none'; // Prevent interaction - - document.body.appendChild(textArea); - - // Store original selection and focus - const currentFocus = document.activeElement; - const selectedRange = document.getSelection().rangeCount > 0 - ? document.getSelection().getRangeAt(0) - : false; - - try { - // Select text inside the textarea - textArea.select(); - // textArea.setSelectionRange(0, 99999); // For mobile potentially - - // Attempt to copy - success = document.execCommand('copy'); - - if (!success) { - console.warn('document.execCommand("copy") returned false.'); - } - } catch (err) { - console.error('Error executing document.execCommand("copy"):', err); - success = false; - } finally { - // Cleanup: remove the temporary element - document.body.removeChild(textArea); - - // Restore original selection and focus - if (selectedRange) { - const selection = document.getSelection(); - selection.removeAllRanges(); // Clear selection from temp textarea - selection.addRange(selectedRange); // Restore original selection - } - if (currentFocus && typeof currentFocus.focus === 'function') { - // Only focus if it's focusable - currentFocus.focus(); // Restore original focus - } - } - - return success; - } - // --- End Helper Function --- - - if (copyTonButton && tonAddressElement) { - copyTonButton.addEventListener('click', async () => { // Use async for await + copyTonButton.addEventListener('click', async () => { const address = (copyTonButton.dataset.address || tonAddressElement.textContent || '').trim(); - if (!address) { console.error('Address to copy is empty.'); alert('Cannot copy: Address is empty.'); @@ -87,202 +23,158 @@ document.addEventListener('DOMContentLoaded', () => { } let copied = false; - let copyMethod = ''; // To track which method worked - // --- METHOD 1: Try Modern Clipboard API (Requires HTTPS or localhost) --- - // Check if API exists AND if we are in a secure context + // Use Modern Clipboard API (Requires HTTPS or localhost) if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(address); copied = true; - copyMethod = 'Clipboard API'; + console.log('Address copied successfully using Clipboard API.'); } catch (err) { - console.warn('navigator.clipboard.writeText failed (maybe permissions?):', err); - // If it failed, we'll try the fallback method below + console.error('navigator.clipboard.writeText failed:', err); + // Don't use fallback, just inform the user } } else { - console.warn('navigator.clipboard not available or context is not secure (HTTPS/localhost required). Trying fallback.'); + console.warn('navigator.clipboard not available or context not secure (HTTPS/localhost required).'); } - // --- METHOD 2: Fallback to execCommand (Legacy) --- - if (!copied) { - copied = copyUsingExecCommand(address); - if (copied) { - copyMethod = 'execCommand'; - } - } - - // --- Provide Feedback --- + // Provide Feedback if (copied) { - console.log(`Address copied successfully using: ${copyMethod}`); const originalText = copyTonButton.textContent; copyTonButton.textContent = 'Copied!'; - copyTonButton.disabled = true; // Disable button temporarily + copyTonButton.disabled = true; setTimeout(() => { copyTonButton.textContent = originalText; copyTonButton.disabled = false; }, 1500); } else { - // If both methods failed - console.error('Failed to copy address using both Clipboard API and execCommand.'); - alert('Failed to copy address. Please copy manually.'); + // Inform user if Clipboard API failed or wasn't available + alert('Failed to copy address automatically. Please copy manually.\n(Requires HTTPS or localhost)'); } }); } else { if (!copyTonButton) console.warn("Element with ID 'copy-ton-button' not found."); if (!tonAddressElement) console.warn("Element with ID 'ton-address' not found."); } - // --- End Copy TON Address --- // --- Last.fm Now Playing --- -const nowPlayingCover = document.getElementById('now-playing-cover'); -const nowPlayingStatus = document.getElementById('now-playing-status'); -const nowPlayingTitleLink = document.getElementById('now-playing-title-link'); -const nowPlayingTitleText = document.getElementById('now-playing-title'); -const nowPlayingArtist = document.getElementById('now-playing-artist'); + const nowPlayingCover = document.getElementById('now-playing-cover'); + const nowPlayingStatus = document.getElementById('now-playing-status'); + const nowPlayingTitleLink = document.getElementById('now-playing-title-link'); + const nowPlayingTitleText = document.getElementById('now-playing-title'); + const nowPlayingArtist = document.getElementById('now-playing-artist'); -const placeholderSvg = "data:image/svg+xml,%3Csvg width='80' height='80' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect width='100%25' height='100%25' fill='%231f3a3a'/%3E%3C/svg%3E"; + const placeholderSvg = "data:image/svg+xml,%3Csvg width='80' height='80' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect width='100%25' height='100%25' fill='%231f3a3a'/%3E%3C/svg%3E"; -let previousTrackId = null; -let previousStatus = null; // can be 'playing', 'not_playing', 'error', 'rate_limited' + let previousTrackId = null; + let previousStatus = null; // 'playing', 'not_playing', 'error', 'rate_limited' -if (!nowPlayingCover || !nowPlayingStatus || !nowPlayingTitleLink || !nowPlayingTitleText || !nowPlayingArtist) { - console.error("One or more Last.fm elements are missing from the DOM."); - // Stop execution if elements are missing - // Consider throwing an error or returning, depending on context - // For this example, we'll just return to prevent further errors. - return; -} - -// Helper function to set the UI to an error/inactive state -function setErrorState(message, statusIdentifier = 'error') { - if (previousStatus !== statusIdentifier) { - nowPlayingStatus.textContent = message; - nowPlayingTitleText.textContent = ''; - nowPlayingArtist.textContent = ''; - if (nowPlayingCover.src !== placeholderSvg) { - nowPlayingCover.src = placeholderSvg; - } - nowPlayingCover.alt = 'Error or inactive state'; - nowPlayingCover.classList.add('error'); // Add error class for visual feedback - - nowPlayingTitleLink.removeAttribute('href'); - nowPlayingTitleLink.classList.add('inactive-link'); - nowPlayingTitleLink.setAttribute('aria-disabled', 'true'); - - previousStatus = statusIdentifier; - previousTrackId = null; + if (!nowPlayingCover || !nowPlayingStatus || !nowPlayingTitleLink || !nowPlayingTitleText || !nowPlayingArtist) { + console.error("One or more Last.fm elements are missing from the DOM."); + return; } -} - -function updateNowPlaying() { - fetch('/api/v1/now_playing') - .then(response => { - // --- START 429 Handling --- - if (response.status === 429) { - // Attempt to parse the JSON body for the custom message - return response.json().then(errorData => { - // Use the message from the server if available - const message = errorData?.message || "Too many requests. Please wait."; - setErrorState(message, 'rate_limited'); - // Throw an error to skip the rest of the .then chain and go to .catch - // This signals that we handled this specific error type already - throw new Error('Rate limit exceeded'); - }).catch(jsonError => { - // If parsing the 429 JSON fails, use a default message - console.error("Could not parse 429 response body:", jsonError); - setErrorState("Too many requests. Please wait.", 'rate_limited'); - throw new Error('Rate limit exceeded'); // Still go to .catch - }); + // Helper function to set the UI to an error/inactive state + function setErrorState(message, statusIdentifier = 'error') { + if (previousStatus !== statusIdentifier) { + nowPlayingStatus.textContent = message; + nowPlayingTitleText.textContent = ''; + nowPlayingArtist.textContent = ''; + if (nowPlayingCover.src !== placeholderSvg) { + nowPlayingCover.src = placeholderSvg; } - // --- END 429 Handling --- + nowPlayingCover.alt = 'Error or inactive state'; + nowPlayingCover.classList.add('error'); + nowPlayingTitleLink.removeAttribute('href'); + nowPlayingTitleLink.classList.add('inactive-link'); + nowPlayingTitleLink.setAttribute('aria-disabled', 'true'); + previousStatus = statusIdentifier; + previousTrackId = null; + } + } - if (!response.ok) { - // Handle other HTTP errors (like 500, 404, etc.) - throw new Error(`HTTP error! status: ${response.status}`); - } - // If response is OK (2xx), parse the JSON - return response.json(); - }) - .then(data => { - // This block only runs if the response was OK (2xx) and not 429 - let currentStatus; - let currentTrackId = null; - - nowPlayingCover.classList.remove('error'); // Remove error class if previous state was error - - // We already know data is not null/undefined if we reached here - if (data.error) { // Handle application-level errors returned in JSON - console.error('API Logic Error:', data.error); - currentStatus = 'error'; - setErrorState(data.error || 'Error fetching data.'); // Use helper - } else if (data.playing && data.artist && data.name) { - currentStatus = 'playing'; - currentTrackId = `${data.artist}-${data.name}`; - } else if (data.playing) { - console.warn('API returned playing:true, but missing artist or name.'); - currentStatus = 'error'; - setErrorState('Incomplete track data received.'); // Use helper - } else { - currentStatus = 'not_playing'; - } - - // Update UI only if state changed or track changed - if (currentStatus !== previousStatus || (currentStatus === 'playing' && currentTrackId !== previousTrackId)) { - - if (currentStatus === 'playing') { - nowPlayingStatus.textContent = ''; // Clear status text - nowPlayingTitleText.textContent = data.name; - nowPlayingArtist.textContent = `by ${data.artist}`; - - const lastfmUrl = `https://www.last.fm/music/${encodeURIComponent(data.artist)}/_/${encodeURIComponent(data.name)}`; - nowPlayingTitleLink.href = lastfmUrl; - nowPlayingTitleLink.classList.remove('inactive-link'); - nowPlayingTitleLink.removeAttribute('aria-disabled'); - - const newCoverSrc = data.image_url || placeholderSvg; - if (nowPlayingCover.src !== newCoverSrc) { - nowPlayingCover.src = newCoverSrc; - } - nowPlayingCover.alt = data.image_url ? `Album cover for ${data.name} by ${data.artist}` : 'Album cover not available'; - - } else if (currentStatus === 'not_playing') { - nowPlayingStatus.textContent = data.message || 'Not listening right now.'; - nowPlayingTitleText.textContent = ''; - nowPlayingArtist.textContent = ''; - if (nowPlayingCover.src !== placeholderSvg) { - nowPlayingCover.src = placeholderSvg; - } - nowPlayingCover.alt = 'Nothing playing'; - - nowPlayingTitleLink.removeAttribute('href'); - nowPlayingTitleLink.classList.add('inactive-link'); - nowPlayingTitleLink.setAttribute('aria-disabled', 'true'); + function updateNowPlaying() { + fetch('/api/v1/now_playing') + .then(response => { + if (response.status === 429) { + return response.json().then(errorData => { + const message = errorData?.message || "Too many requests. Please wait."; + setErrorState(message, 'rate_limited'); + throw new Error('Rate limit exceeded'); // Skip to .catch + }).catch(jsonError => { + console.error("Could not parse 429 response body:", jsonError); + setErrorState("Too many requests. Please wait.", 'rate_limited'); + throw new Error('Rate limit exceeded'); + }); } - // Error states are handled above or in .catch using setErrorState + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + let currentStatus; + let currentTrackId = null; + nowPlayingCover.classList.remove('error'); - previousStatus = currentStatus; // Update previous status only on successful processing - previousTrackId = currentTrackId; - } - }) - .catch(error => { - // Catch network errors, HTTP errors (thrown from !response.ok), - // and the specific 'Rate limit exceeded' error we threw. - console.error('Fetch Error:', error); + if (data.error) { + console.error('API Logic Error:', data.error); + currentStatus = 'error'; + setErrorState(data.error || 'Error fetching data.'); + } else if (data.playing && data.artist && data.name) { + currentStatus = 'playing'; + currentTrackId = `${data.artist}-${data.name}`; + } else if (data.playing) { + console.warn('API returned playing:true, but missing artist or name.'); + currentStatus = 'error'; + setErrorState('Incomplete track data received.'); + } else { + currentStatus = 'not_playing'; + } - // Avoid setting generic error if it was the rate limit we already handled - if (error.message !== 'Rate limit exceeded') { - // Use the helper function for generic network/HTTP errors - setErrorState('Could not connect or error occurred.'); - } - // If it *was* 'Rate limit exceeded', the UI is already set by the 429 handler. - }); -} + // Update UI only if state or track changed + if (currentStatus !== previousStatus || (currentStatus === 'playing' && currentTrackId !== previousTrackId)) { + if (currentStatus === 'playing') { + nowPlayingStatus.textContent = ''; + nowPlayingTitleText.textContent = data.name; + nowPlayingArtist.textContent = `by ${data.artist}`; + nowPlayingTitleLink.href = `https://www.last.fm/music/${encodeURIComponent(data.artist)}/_/${encodeURIComponent(data.name)}`; + nowPlayingTitleLink.classList.remove('inactive-link'); + nowPlayingTitleLink.removeAttribute('aria-disabled'); + const newCoverSrc = data.image_url || placeholderSvg; + if (nowPlayingCover.src !== newCoverSrc) { + nowPlayingCover.src = newCoverSrc; + } + nowPlayingCover.alt = data.image_url ? `Album cover for ${data.name} by ${data.artist}` : 'Album cover not available'; + } else if (currentStatus === 'not_playing') { + nowPlayingStatus.textContent = data.message || 'Not listening right now.'; + nowPlayingTitleText.textContent = ''; + nowPlayingArtist.textContent = ''; + if (nowPlayingCover.src !== placeholderSvg) { + nowPlayingCover.src = placeholderSvg; + } + nowPlayingCover.alt = 'Nothing playing'; + nowPlayingTitleLink.removeAttribute('href'); + nowPlayingTitleLink.classList.add('inactive-link'); + nowPlayingTitleLink.setAttribute('aria-disabled', 'true'); + } + // Error states handled via setErrorState -updateNowPlaying(); -setInterval(updateNowPlaying, 30000); -// --- End Last.fm Now Playing --- + previousStatus = currentStatus; + previousTrackId = currentTrackId; + } + }) + .catch(error => { + console.error('Fetch Error:', error); + // Avoid setting generic error if it was the rate limit we already handled + if (error.message !== 'Rate limit exceeded') { + setErrorState('Could not connect or error occurred.'); + } + }); + } + + updateNowPlaying(); + setInterval(updateNowPlaying, 30000); }); \ No newline at end of file