pbio-site/static/js/script.js
2025-04-19 12:17:00 +00:00

288 lines
No EOL
13 KiB
JavaScript

// script.js
// Copyright (c) 2025 snfsx.xyz
// Licensed under the MIT License. See https://opensource.org/licenses/MIT
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
const address = (copyTonButton.dataset.address || tonAddressElement.textContent || '').trim();
if (!address) {
console.error('Address to copy is empty.');
alert('Cannot copy: Address is empty.');
return;
}
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
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(address);
copied = true;
copyMethod = 'Clipboard API';
} catch (err) {
console.warn('navigator.clipboard.writeText failed (maybe permissions?):', err);
// If it failed, we'll try the fallback method below
}
} else {
console.warn('navigator.clipboard not available or context is not secure (HTTPS/localhost required). Trying fallback.');
}
// --- METHOD 2: Fallback to execCommand (Legacy) ---
if (!copied) {
copied = copyUsingExecCommand(address);
if (copied) {
copyMethod = 'execCommand';
}
}
// --- Provide Feedback ---
if (copied) {
console.log(`Address copied successfully using: ${copyMethod}`);
const originalText = copyTonButton.textContent;
copyTonButton.textContent = 'Copied!';
copyTonButton.disabled = true; // Disable button temporarily
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.');
}
});
} 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 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'
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;
}
}
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
});
}
// --- END 429 Handling ---
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');
}
// Error states are handled above or in .catch using setErrorState
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);
// 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.
});
}
updateNowPlaying();
setInterval(updateNowPlaying, 30000);
// --- End Last.fm Now Playing ---
});