Updated script.js
This commit is contained in:
parent
bf08fbfc04
commit
739822883a
1 changed files with 116 additions and 224 deletions
|
@ -7,79 +7,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if ('scrollRestoration' in history) {
|
if ('scrollRestoration' in history) {
|
||||||
history.scrollRestoration = 'manual';
|
history.scrollRestoration = 'manual';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Прокручиваем в самый верх
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
// --- Copy TON Address ---
|
// --- Copy TON Address ---
|
||||||
const copyTonButton = document.getElementById('copy-ton-button');
|
const copyTonButton = document.getElementById('copy-ton-button');
|
||||||
const tonAddressElement = document.getElementById('ton-address');
|
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) {
|
if (copyTonButton && tonAddressElement) {
|
||||||
copyTonButton.addEventListener('click', async () => { // Use async for await
|
copyTonButton.addEventListener('click', async () => {
|
||||||
const address = (copyTonButton.dataset.address || tonAddressElement.textContent || '').trim();
|
const address = (copyTonButton.dataset.address || tonAddressElement.textContent || '').trim();
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
console.error('Address to copy is empty.');
|
console.error('Address to copy is empty.');
|
||||||
alert('Cannot copy: Address is empty.');
|
alert('Cannot copy: Address is empty.');
|
||||||
|
@ -87,52 +23,39 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let copied = false;
|
let copied = false;
|
||||||
let copyMethod = ''; // To track which method worked
|
|
||||||
|
|
||||||
// --- METHOD 1: Try Modern Clipboard API (Requires HTTPS or localhost) ---
|
// Use Modern Clipboard API (Requires HTTPS or localhost)
|
||||||
// Check if API exists AND if we are in a secure context
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(address);
|
await navigator.clipboard.writeText(address);
|
||||||
copied = true;
|
copied = true;
|
||||||
copyMethod = 'Clipboard API';
|
console.log('Address copied successfully using Clipboard API.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('navigator.clipboard.writeText failed (maybe permissions?):', err);
|
console.error('navigator.clipboard.writeText failed:', err);
|
||||||
// If it failed, we'll try the fallback method below
|
// Don't use fallback, just inform the user
|
||||||
}
|
}
|
||||||
} else {
|
} 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) ---
|
// Provide Feedback
|
||||||
if (!copied) {
|
|
||||||
copied = copyUsingExecCommand(address);
|
|
||||||
if (copied) {
|
if (copied) {
|
||||||
copyMethod = 'execCommand';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Provide Feedback ---
|
|
||||||
if (copied) {
|
|
||||||
console.log(`Address copied successfully using: ${copyMethod}`);
|
|
||||||
const originalText = copyTonButton.textContent;
|
const originalText = copyTonButton.textContent;
|
||||||
copyTonButton.textContent = 'Copied!';
|
copyTonButton.textContent = 'Copied!';
|
||||||
copyTonButton.disabled = true; // Disable button temporarily
|
copyTonButton.disabled = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copyTonButton.textContent = originalText;
|
copyTonButton.textContent = originalText;
|
||||||
copyTonButton.disabled = false;
|
copyTonButton.disabled = false;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
// If both methods failed
|
// Inform user if Clipboard API failed or wasn't available
|
||||||
console.error('Failed to copy address using both Clipboard API and execCommand.');
|
alert('Failed to copy address automatically. Please copy manually.\n(Requires HTTPS or localhost)');
|
||||||
alert('Failed to copy address. Please copy manually.');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!copyTonButton) console.warn("Element with ID 'copy-ton-button' not found.");
|
if (!copyTonButton) console.warn("Element with ID 'copy-ton-button' not found.");
|
||||||
if (!tonAddressElement) console.warn("Element with ID 'ton-address' not found.");
|
if (!tonAddressElement) console.warn("Element with ID 'ton-address' not found.");
|
||||||
}
|
}
|
||||||
// --- End Copy TON Address ---
|
|
||||||
|
|
||||||
|
|
||||||
// --- Last.fm Now Playing ---
|
// --- Last.fm Now Playing ---
|
||||||
|
@ -145,13 +68,10 @@ 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 previousTrackId = null;
|
||||||
let previousStatus = null; // can be 'playing', 'not_playing', 'error', 'rate_limited'
|
let previousStatus = null; // 'playing', 'not_playing', 'error', 'rate_limited'
|
||||||
|
|
||||||
if (!nowPlayingCover || !nowPlayingStatus || !nowPlayingTitleLink || !nowPlayingTitleText || !nowPlayingArtist) {
|
if (!nowPlayingCover || !nowPlayingStatus || !nowPlayingTitleLink || !nowPlayingTitleText || !nowPlayingArtist) {
|
||||||
console.error("One or more Last.fm elements are missing from the DOM.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,89 +85,68 @@ function setErrorState(message, statusIdentifier = 'error') {
|
||||||
nowPlayingCover.src = placeholderSvg;
|
nowPlayingCover.src = placeholderSvg;
|
||||||
}
|
}
|
||||||
nowPlayingCover.alt = 'Error or inactive state';
|
nowPlayingCover.alt = 'Error or inactive state';
|
||||||
nowPlayingCover.classList.add('error'); // Add error class for visual feedback
|
nowPlayingCover.classList.add('error');
|
||||||
|
|
||||||
nowPlayingTitleLink.removeAttribute('href');
|
nowPlayingTitleLink.removeAttribute('href');
|
||||||
nowPlayingTitleLink.classList.add('inactive-link');
|
nowPlayingTitleLink.classList.add('inactive-link');
|
||||||
nowPlayingTitleLink.setAttribute('aria-disabled', 'true');
|
nowPlayingTitleLink.setAttribute('aria-disabled', 'true');
|
||||||
|
|
||||||
previousStatus = statusIdentifier;
|
previousStatus = statusIdentifier;
|
||||||
previousTrackId = null;
|
previousTrackId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateNowPlaying() {
|
function updateNowPlaying() {
|
||||||
fetch('/api/v1/now_playing')
|
fetch('/api/v1/now_playing')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// --- START 429 Handling ---
|
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
// Attempt to parse the JSON body for the custom message
|
|
||||||
return response.json().then(errorData => {
|
return response.json().then(errorData => {
|
||||||
// Use the message from the server if available
|
|
||||||
const message = errorData?.message || "Too many requests. Please wait.";
|
const message = errorData?.message || "Too many requests. Please wait.";
|
||||||
setErrorState(message, 'rate_limited');
|
setErrorState(message, 'rate_limited');
|
||||||
// Throw an error to skip the rest of the .then chain and go to .catch
|
throw new Error('Rate limit exceeded'); // Skip to .catch
|
||||||
// This signals that we handled this specific error type already
|
|
||||||
throw new Error('Rate limit exceeded');
|
|
||||||
}).catch(jsonError => {
|
}).catch(jsonError => {
|
||||||
// If parsing the 429 JSON fails, use a default message
|
|
||||||
console.error("Could not parse 429 response body:", jsonError);
|
console.error("Could not parse 429 response body:", jsonError);
|
||||||
setErrorState("Too many requests. Please wait.", 'rate_limited');
|
setErrorState("Too many requests. Please wait.", 'rate_limited');
|
||||||
throw new Error('Rate limit exceeded'); // Still go to .catch
|
throw new Error('Rate limit exceeded');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --- END 429 Handling ---
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Handle other HTTP errors (like 500, 404, etc.)
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
// If response is OK (2xx), parse the JSON
|
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// This block only runs if the response was OK (2xx) and not 429
|
|
||||||
let currentStatus;
|
let currentStatus;
|
||||||
let currentTrackId = null;
|
let currentTrackId = null;
|
||||||
|
nowPlayingCover.classList.remove('error');
|
||||||
|
|
||||||
nowPlayingCover.classList.remove('error'); // Remove error class if previous state was error
|
if (data.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);
|
console.error('API Logic Error:', data.error);
|
||||||
currentStatus = 'error';
|
currentStatus = 'error';
|
||||||
setErrorState(data.error || 'Error fetching data.'); // Use helper
|
setErrorState(data.error || 'Error fetching data.');
|
||||||
} else if (data.playing && data.artist && data.name) {
|
} else if (data.playing && data.artist && data.name) {
|
||||||
currentStatus = 'playing';
|
currentStatus = 'playing';
|
||||||
currentTrackId = `${data.artist}-${data.name}`;
|
currentTrackId = `${data.artist}-${data.name}`;
|
||||||
} else if (data.playing) {
|
} else if (data.playing) {
|
||||||
console.warn('API returned playing:true, but missing artist or name.');
|
console.warn('API returned playing:true, but missing artist or name.');
|
||||||
currentStatus = 'error';
|
currentStatus = 'error';
|
||||||
setErrorState('Incomplete track data received.'); // Use helper
|
setErrorState('Incomplete track data received.');
|
||||||
} else {
|
} else {
|
||||||
currentStatus = 'not_playing';
|
currentStatus = 'not_playing';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI only if state changed or track changed
|
// Update UI only if state or track changed
|
||||||
if (currentStatus !== previousStatus || (currentStatus === 'playing' && currentTrackId !== previousTrackId)) {
|
if (currentStatus !== previousStatus || (currentStatus === 'playing' && currentTrackId !== previousTrackId)) {
|
||||||
|
|
||||||
if (currentStatus === 'playing') {
|
if (currentStatus === 'playing') {
|
||||||
nowPlayingStatus.textContent = ''; // Clear status text
|
nowPlayingStatus.textContent = '';
|
||||||
nowPlayingTitleText.textContent = data.name;
|
nowPlayingTitleText.textContent = data.name;
|
||||||
nowPlayingArtist.textContent = `by ${data.artist}`;
|
nowPlayingArtist.textContent = `by ${data.artist}`;
|
||||||
|
nowPlayingTitleLink.href = `https://www.last.fm/music/${encodeURIComponent(data.artist)}/_/${encodeURIComponent(data.name)}`;
|
||||||
const lastfmUrl = `https://www.last.fm/music/${encodeURIComponent(data.artist)}/_/${encodeURIComponent(data.name)}`;
|
|
||||||
nowPlayingTitleLink.href = lastfmUrl;
|
|
||||||
nowPlayingTitleLink.classList.remove('inactive-link');
|
nowPlayingTitleLink.classList.remove('inactive-link');
|
||||||
nowPlayingTitleLink.removeAttribute('aria-disabled');
|
nowPlayingTitleLink.removeAttribute('aria-disabled');
|
||||||
|
|
||||||
const newCoverSrc = data.image_url || placeholderSvg;
|
const newCoverSrc = data.image_url || placeholderSvg;
|
||||||
if (nowPlayingCover.src !== newCoverSrc) {
|
if (nowPlayingCover.src !== newCoverSrc) {
|
||||||
nowPlayingCover.src = newCoverSrc;
|
nowPlayingCover.src = newCoverSrc;
|
||||||
}
|
}
|
||||||
nowPlayingCover.alt = data.image_url ? `Album cover for ${data.name} by ${data.artist}` : 'Album cover not available';
|
nowPlayingCover.alt = data.image_url ? `Album cover for ${data.name} by ${data.artist}` : 'Album cover not available';
|
||||||
|
|
||||||
} else if (currentStatus === 'not_playing') {
|
} else if (currentStatus === 'not_playing') {
|
||||||
nowPlayingStatus.textContent = data.message || 'Not listening right now.';
|
nowPlayingStatus.textContent = data.message || 'Not listening right now.';
|
||||||
nowPlayingTitleText.textContent = '';
|
nowPlayingTitleText.textContent = '';
|
||||||
|
@ -256,33 +155,26 @@ function updateNowPlaying() {
|
||||||
nowPlayingCover.src = placeholderSvg;
|
nowPlayingCover.src = placeholderSvg;
|
||||||
}
|
}
|
||||||
nowPlayingCover.alt = 'Nothing playing';
|
nowPlayingCover.alt = 'Nothing playing';
|
||||||
|
|
||||||
nowPlayingTitleLink.removeAttribute('href');
|
nowPlayingTitleLink.removeAttribute('href');
|
||||||
nowPlayingTitleLink.classList.add('inactive-link');
|
nowPlayingTitleLink.classList.add('inactive-link');
|
||||||
nowPlayingTitleLink.setAttribute('aria-disabled', 'true');
|
nowPlayingTitleLink.setAttribute('aria-disabled', 'true');
|
||||||
}
|
}
|
||||||
// Error states are handled above or in .catch using setErrorState
|
// Error states handled via setErrorState
|
||||||
|
|
||||||
previousStatus = currentStatus; // Update previous status only on successful processing
|
previousStatus = currentStatus;
|
||||||
previousTrackId = currentTrackId;
|
previousTrackId = currentTrackId;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.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);
|
console.error('Fetch Error:', error);
|
||||||
|
|
||||||
// Avoid setting generic error if it was the rate limit we already handled
|
// Avoid setting generic error if it was the rate limit we already handled
|
||||||
if (error.message !== 'Rate limit exceeded') {
|
if (error.message !== 'Rate limit exceeded') {
|
||||||
// Use the helper function for generic network/HTTP errors
|
|
||||||
setErrorState('Could not connect or error occurred.');
|
setErrorState('Could not connect or error occurred.');
|
||||||
}
|
}
|
||||||
// If it *was* 'Rate limit exceeded', the UI is already set by the 429 handler.
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNowPlaying();
|
updateNowPlaying();
|
||||||
setInterval(updateNowPlaying, 30000);
|
setInterval(updateNowPlaying, 30000);
|
||||||
// --- End Last.fm Now Playing ---
|
|
||||||
|
|
||||||
});
|
});
|
Loading…
Add table
Reference in a new issue