Loads of stuff

This commit is contained in:
Dan 2026-01-07 14:32:06 +00:00
parent fd015bd52c
commit a3b9bd2680
15 changed files with 2310 additions and 208 deletions

70
assets/js/code-copy.js Normal file
View file

@ -0,0 +1,70 @@
// Add copy buttons to all code blocks
document.addEventListener("DOMContentLoaded", function () {
// Find all <pre> elements that contain <code>
const codeBlocks = document.querySelectorAll("pre code");
codeBlocks.forEach((codeBlock) => {
const pre = codeBlock.parentElement;
// Create wrapper for positioning
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
// Wrap the pre element
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
// Create copy button
const copyButton = document.createElement("button");
copyButton.className = "code-copy-btn";
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
`;
copyButton.setAttribute("aria-label", "Copy code to clipboard");
// Add click handler
copyButton.addEventListener("click", async () => {
const code = codeBlock.textContent;
try {
await navigator.clipboard.writeText(code);
// Show success feedback
copyButton.classList.add("copied");
// Reset after 2 seconds
setTimeout(() => {
copyButton.classList.remove("copied");
}, 2000);
} catch (err) {
console.error("Failed to copy code:", err);
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = code;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
copyButton.classList.add("copied");
setTimeout(() => {
copyButton.classList.remove("copied");
}, 2000);
} catch (err2) {
console.error("Fallback copy failed:", err2);
}
document.body.removeChild(textArea);
}
});
wrapper.appendChild(copyButton);
});
});

260
assets/js/lastfm-stats.js Normal file
View file

@ -0,0 +1,260 @@
// Last.fm Stats Interactive Module
(function () {
"use strict";
const LASTFM_API_URL = "https://ws.audioscrobbler.com/2.0/";
const LASTFM_API_KEY = "3a4fef48fecc593d25e0f9a40df1fefe";
// Store current stats for export
let currentStats = {
artists: [],
totalTracks: 0,
period: "",
username: "",
};
// Calculate timestamps based on period
function getTimestamps(period) {
const now = Math.floor(Date.now() / 1000);
let from;
if (period === "7day") {
from = now - 7 * 24 * 60 * 60; // 7 days
} else if (period === "1month") {
from = now - 30 * 24 * 60 * 60; // 30 days
}
return { from, to: now };
}
// Fetch top artists for the specified period
async function fetchTopArtists(username, period) {
const url = `${LASTFM_API_URL}?method=user.gettopartists&user=${username}&api_key=${LASTFM_API_KEY}&format=json&period=${period}&limit=5`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch top artists: ${response.statusText}`);
}
const data = await response.json();
// Check for Last.fm API errors
if (data.error) {
throw new Error(data.message || "Last.fm API error");
}
return data.topartists?.artist || [];
}
// Fetch recent tracks to count total scrobbles in period
async function fetchTrackCount(username, period) {
const { from, to } = getTimestamps(period);
const url = `${LASTFM_API_URL}?method=user.getrecenttracks&user=${username}&api_key=${LASTFM_API_KEY}&format=json&from=${from}&to=${to}&limit=1`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch track count: ${response.statusText}`);
}
const data = await response.json();
// Check for Last.fm API errors
if (data.error) {
throw new Error(data.message || "Last.fm API error");
}
return data.recenttracks?.["@attr"]?.total || 0;
}
// Generate markdown format
function generateMarkdown() {
const periodText =
currentStats.period === "7day" ? "Past Week" : "Past Month";
let markdown = `## Last.fm Stats - ${periodText}\n\n`;
markdown += `**Total Tracks:** ${currentStats.totalTracks}\n\n`;
markdown += `**Top 5 Artists:**\n\n`;
currentStats.artists.forEach((artist) => {
markdown += `- [${artist.name}](${artist.url}) - ${artist.playcount} plays\n`;
});
return markdown;
}
// Generate plain text format
function generatePlainText() {
const periodText =
currentStats.period === "7day" ? "Past Week" : "Past Month";
let text = `Last.fm Stats - ${periodText}\n\n`;
text += `Total Tracks: ${currentStats.totalTracks}\n\n`;
text += `Top 5 Artists:\n\n`;
currentStats.artists.forEach((artist) => {
text += `- ${artist.name} - ${artist.playcount} plays\n`;
});
return text;
}
// Copy to clipboard
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
const originalText = button.textContent;
button.textContent = "Copied!";
button.classList.add("copied");
setTimeout(() => {
button.textContent = originalText;
button.classList.remove("copied");
}, 2000);
} catch (err) {
console.error("Failed to copy:", err);
alert("Failed to copy to clipboard");
}
}
// Display the stats
function displayStats(artists, totalTracks, period, username) {
const artistsList = document.getElementById("top-artists");
const totalTracksEl = document.getElementById("total-tracks");
// Store current stats for export
currentStats = { artists, totalTracks, period, username };
// Update total tracks
totalTracksEl.textContent = totalTracks;
// Clear and populate artists list
artistsList.innerHTML = "";
if (artists.length === 0) {
artistsList.innerHTML = "<li>No artists found for this period</li>";
return;
}
artists.forEach((artist) => {
const li = document.createElement("li");
li.innerHTML = `<a href="${artist.url}" target="_blank">${artist.name}</a> - ${artist.playcount} plays`;
artistsList.appendChild(li);
});
// Show export buttons
const exportButtons = document.getElementById("export-buttons");
if (exportButtons) {
exportButtons.style.display = "flex";
}
}
// Show/hide UI elements
function setLoadingState(isLoading) {
const loading = document.getElementById("stats-loading");
const content = document.getElementById("stats-content");
const error = document.getElementById("stats-error");
const results = document.getElementById("stats-results");
results.style.display = "block";
if (isLoading) {
loading.style.display = "block";
content.style.display = "none";
error.style.display = "none";
} else {
loading.style.display = "none";
}
}
function showError(message) {
const error = document.getElementById("stats-error");
const errorMessage = document.getElementById("error-message");
const content = document.getElementById("stats-content");
error.style.display = "block";
content.style.display = "none";
errorMessage.textContent = message;
}
function showContent() {
const error = document.getElementById("stats-error");
const content = document.getElementById("stats-content");
error.style.display = "none";
content.style.display = "block";
}
// Main fetch function
async function fetchStats() {
const username = document.getElementById("lastfm-username").value.trim();
const period = document.getElementById("time-period").value;
if (!username) {
showError("Please enter a Last.fm username");
return;
}
setLoadingState(true);
try {
// Fetch both stats in parallel
const [artists, totalTracks] = await Promise.all([
fetchTopArtists(username, period),
fetchTrackCount(username, period),
]);
displayStats(artists, totalTracks, period, username);
showContent();
} catch (error) {
console.error("Error fetching Last.fm stats:", error);
showError(
error.message ||
"Failed to fetch stats. Please check the username and try again.",
);
} finally {
setLoadingState(false);
}
}
// Initialize when DOM is ready
function init() {
const fetchButton = document.getElementById("fetch-stats");
const usernameInput = document.getElementById("lastfm-username");
const copyMarkdownBtn = document.getElementById("copy-markdown");
const copyPlainTextBtn = document.getElementById("copy-plaintext");
if (!fetchButton || !usernameInput) {
return; // Not on the stats page
}
// Fetch stats on button click
fetchButton.addEventListener("click", fetchStats);
// Also fetch on Enter key in username input
usernameInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
fetchStats();
}
});
// Copy buttons
if (copyMarkdownBtn) {
copyMarkdownBtn.addEventListener("click", () => {
const markdown = generateMarkdown();
copyToClipboard(markdown, copyMarkdownBtn);
});
}
if (copyPlainTextBtn) {
copyPlainTextBtn.addEventListener("click", () => {
const plainText = generatePlainText();
copyToClipboard(plainText, copyPlainTextBtn);
});
}
}
// Run init when DOM is loaded
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

33
assets/js/pgp-copy.js Normal file
View file

@ -0,0 +1,33 @@
// PGP Key copy functionality
document.addEventListener('DOMContentLoaded', function() {
const copyButtons = document.querySelectorAll('.pgp-copy-trigger');
copyButtons.forEach(button => {
button.addEventListener('click', async function() {
const feedback = button.closest('.contact-pgp').querySelector('.pgp-copy-feedback');
try {
const response = await fetch('/publickey.asc');
const pgpKey = await response.text();
await navigator.clipboard.writeText(pgpKey);
feedback.textContent = 'PGP key copied to clipboard!';
feedback.classList.add('show', 'success');
setTimeout(() => {
feedback.classList.remove('show');
}, 3000);
} catch (err) {
console.error('Failed to copy PGP key:', err);
feedback.textContent = 'Failed to copy key';
feedback.classList.add('show', 'error');
setTimeout(() => {
feedback.classList.remove('show');
}, 3000);
}
});
});
});