---------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>E-Scooter Registration Plate Color</title> <style> body { font-family: Arial, sans-serif; max-width: 450px; margin: 50px auto; padding: 20px; border: 2px solid #ccc; border-radius: 10px; text-align: center; } .color-box { font-size: 1.5em; font-weight: bold; } .Black { color: black; } .Blue { color: blue; } .Green { color: green; } .next small { display: block; margin-top: 5px; font-size: 0.8em; color: #555; } details { margin-top: 20px; text-align: left; } .status { margin-top: 15px; font-weight: bold; } .valid { color: green; } .upcoming { color: blue; } .expired { color: red; } .radio-group { margin-top: 10px; display: flex; gap: 10px; } .radio-group label { display: flex; align-items: center; gap: 5px; cursor: pointer; } </style> </head> <body> <h2>Current Insurance Sticker</h2> <div class="current"> Current Color: <span class="color-box" id="currentColor"></span><br> Valid for: <span id="timeLeft"></span> </div> <details> <summary>Test Sticker Color (Red, Green, Blue)</summary> (Select the color of the sticker) <div class="radio-group"> <label><input type="radio" name="colorTest" value="Red"> Black</label> <label><input type="radio" name="colorTest" value="Green"> Green</label> <label><input type="radio" name="colorTest" value="Blue"> Blue</label> </div> <div id="colorStatus" class="status"></div> <div class="next"> Next Color: <span class="color-box" id="nextColor"></span> <small>from <span id="nextStart"></span></small> </div> </details> <script> const colors = ['Black', 'Blue', 'Green']; const startYear = 2023; const today = new Date(); const currentYear = today.getMonth() >= 2 ? today.getFullYear() : today.getFullYear() - 1; const yearIndex = currentYear - startYear; const currentColor = colors[yearIndex % colors.length]; const nextColor = colors[(yearIndex + 1) % colors.length]; const endDate = new Date(currentYear + 1, 1, 28); if ((currentYear + 1) % 4 === 0 && ((currentYear + 1) % 100 !== 0 || (currentYear + 1) % 400 === 0)) { endDate.setDate(29); } const nextStart = new Date(currentYear + 1, 2, 1); const msLeft = endDate.getTime() - today.getTime(); const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24)); document.getElementById('currentColor').textContent = currentColor; document.getElementById('currentColor').classList.add(currentColor); document.getElementById('nextColor').textContent = nextColor; document.getElementById('nextColor').classList.add(nextColor); document.getElementById('timeLeft').textContent = daysLeft + ' days'; document.getElementById('nextStart').textContent = nextStart.toLocaleDateString('en-GB'); const radios = document.querySelectorAll('input[name="colorTest"]'); radios.forEach(radio => { radio.addEventListener('change', () => { const input = radio.value; const statusEl = document.getElementById('colorStatus'); let message = ''; let cssClass = ''; const normalized = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase(); const mappedColor = normalized === 'Red' ? 'Black' : normalized; let expirationDate; if (mappedColor === 'Black') { expirationDate = endDate; } else if (mappedColor === 'Blue') { expirationDate = new Date(currentYear, 2, 28); } else if (mappedColor === 'Green') { expirationDate = new Date(currentYear, 5, 30); } const expiredMs = today.getTime() - expirationDate.getTime(); const expiredDays = Math.floor(expiredMs / (1000 * 60 * 60 * 24)); const expiredMonths = Math.floor(expiredDays / 30); if (mappedColor === currentColor) { message = 'This color is currently valid.'; cssClass = 'valid'; } else if (mappedColor === nextColor) { message = `This color will be valid soon (from ${nextStart.toLocaleDateString('en-GB')}).`; cssClass = 'upcoming'; } else { message = `This color has expired. It expired ${expiredDays} days (${expiredMonths} months) ago.`; cssClass = 'expired'; } statusEl.textContent = message; statusEl.className = 'status ' + cssClass; }); }); </script> </body> </html> Fedi find latest posts (excluding boosts) ------------ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <details><summary>Profile to post id resolve</summary> Enter a Fediverse account (either @username@instance.com or full URL like https://woof.tech/@dr_muesli)<br> <input type="text" id="accountHandle" placeholder="Enter username or full URL"> <button onclick="getFirstPost()">Find First Post</button> <div id="result"></div></details> <script> function parseHandle(handle) { let username, instance; const urlRegex = /^(https?:\/\/)([^\/]+)\/(.*)$/; const match = handle.match(urlRegex); if (match) { username = match[3].startsWith('@') ? match[3].substring(1) : match[3]; instance = match[2]; } else { const parts = handle.split('@'); if (parts.length === 2) { username = parts[0].substring(1); instance = parts[1]; } } return { username, instance }; } async function getFirstPost() { const handle = document.getElementById('accountHandle').value.trim(); if (!handle) { alert("Please enter a valid Fediverse handle."); return; } const { username, instance } = parseHandle(handle); if (!username || !instance) { alert("Invalid handle format. Please enter a valid Fediverse account or URL."); return; } try { const lookupUrl = `https://${instance}/api/v1/accounts/lookup?acct=${username}`; const lookupResponse = await fetch(lookupUrl); if (!lookupResponse.ok) { const errorText = await lookupResponse.text(); throw new Error(`Error fetching user data: ${lookupResponse.status} - ${errorText}`); } const userData = await lookupResponse.json(); const userId = userData.id; if (!userId) { throw new Error('User ID not found.'); } const statusesUrl = `https://${instance}/api/v1/accounts/${userId}/statuses`; const statusesResponse = await fetch(statusesUrl); if (!statusesResponse.ok) { const errorText = await statusesResponse.text(); throw new Error(`Error fetching posts: ${statusesResponse.status} - ${errorText}`); } const posts = await statusesResponse.json(); if (!Array.isArray(posts) || posts.length === 0) { document.getElementById('result').innerHTML = 'No posts found.'; } else { const firstPost = posts[posts.length - 1]; if (firstPost && firstPost.url) { const firstPostUrl = firstPost.url; document.getElementById('result').innerHTML = ` <p>First post URL:</p> <a href="${firstPostUrl}" target="_blank">${firstPostUrl}</a> `; } else { document.getElementById('result').innerHTML = 'No URL found for the first post.'; } } } catch (error) { document.getElementById('result').innerHTML = `Error: ${error.message}`; } } </script> <title>Fediverse Post Viewer</title> <style> body { font-family: Arial, sans-serif; padding: 20px; } input[type="text"] { width: 100%; padding: 10px; margin-bottom: 10px; } button { padding: 10px 20px; } .post { border: 1px solid #ccc; padding: 10px; margin: 10px 0; } .post-date { color: gray; font-size: 0.9em; } .post-content img { vertical-align: middle; } </style> </head> <body> <h2>Enter a Fediverse Post URL</h2> <input type="text" id="postUrlInput" placeholder="e.g. https://example.social/@user/1234567890"> <button onclick="processUrl()">Fetch Posts</button> <div id="handleDisplay"></div> <div id="errorDisplay" style="color:red;"></div> <div id="posts"></div> <button id="fetchNextBatchButton" style="display:none;" onclick="fetchNextBatch()">Fetch Next Batch</button> <script> let lastPostId = null; let userApiUrl = ''; let domain = ''; let emojis = []; function getQueryParam(name) { const params = new URLSearchParams(window.location.search); return params.get(name); } async function fetchWithFallback(url, options = {}) { try { const res = await fetch(url, options); if (!res.ok) throw new Error(`HTTP error ${res.status}`); return await res.json(); } catch (err) { console.warn('Direct fetch failed, trying proxy...'); const proxiedUrl = `https://alceawis.de/cors.php?query=${encodeURIComponent(url)}`; const proxyRes = await fetch(proxiedUrl, options); if (!proxyRes.ok) throw new Error(`Proxy fetch failed: ${proxyRes.status}`); return await proxyRes.json(); } } async function fetchEmojis(domain) { const emojiUrl = `https://${domain}/api/v1/custom_emojis`; try { return await fetchWithFallback(emojiUrl); } catch (err) { console.error('Error fetching custom emojis:', err); return []; } } function replaceEmojiShortcodes(content, emojis) { emojis.forEach(emoji => { const shortcode = `:${emoji.shortcode}:`; const emojiImg = `<img src="${emoji.url}" alt="${emoji.shortcode}" title="${emoji.shortcode}" style="width: 1.2em; height: 1.2em; vertical-align: middle;" />`; const regex = new RegExp(`(:${emoji.shortcode}:)`, 'g'); content = content.replace(regex, emojiImg); }); return content; } async function fetchPosts() { let url = userApiUrl; if (lastPostId) { url = `${url}&max_id=${lastPostId}`; } try { const posts = await fetchWithFallback(url); const container = document.getElementById('posts'); if (posts.length === 0) { document.getElementById('fetchNextBatchButton').style.display = 'none'; return; } posts.forEach(post => { const div = document.createElement('div'); div.className = 'post'; let postContent = post.content; postContent = replaceEmojiShortcodes(postContent, emojis); div.innerHTML = ` <div class="post-date">${new Date(post.created_at).toLocaleString()}</div> <div class="post-content">${postContent}</div> <a href="${post.url}" target="_blank">View Post</a> `; container.appendChild(div); lastPostId = post.id; // Update the last post ID }); document.getElementById('fetchNextBatchButton').style.display = 'inline-block'; } catch (err) { console.error(err); document.getElementById('errorDisplay').textContent = `Error: ${err.message}`; } } async function processUrl() { const input = document.getElementById('postUrlInput').value.trim(); document.getElementById('errorDisplay').textContent = ''; document.getElementById('handleDisplay').textContent = ''; document.getElementById('posts').innerHTML = ''; document.getElementById('fetchNextBatchButton').style.display = 'none'; if (!input) return; let postUrl; try { postUrl = new URL(input); } catch (e) { document.getElementById('errorDisplay').textContent = 'Invalid URL.'; return; } domain = postUrl.hostname; const pathname = postUrl.pathname; const parts = pathname.split('/'); const postId = parts[parts.length - 1]; const apiUrl = `https://${domain}/api/v1/statuses/${postId}`; try { const postData = await fetchWithFallback(apiUrl); const account = postData.account; const acct = account.acct.includes('@') ? account.acct : `${account.acct}@${domain}`; userApiUrl = `https://${domain}/api/v1/accounts/${account.id}/statuses?limit=30&exclude_reblogs=true`; document.getElementById('handleDisplay').textContent = `User Handle: ${acct}`; emojis = await fetchEmojis(domain); fetchPosts(); } catch (err) { console.error(err); document.getElementById('errorDisplay').textContent = `Error: ${err.message}`; } } function fetchNextBatch() { fetchPosts(); } window.addEventListener('load', () => { const urlParam = getQueryParam('url'); if (urlParam) { document.getElementById('postUrlInput').value = urlParam; processUrl(); } }); </script> </body> </html> Icon Overlay creator --------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Layered Image Editor</title> <style> body { font-family: sans-serif; text-align: center; margin-top: 20px; } canvas { border: 1px solid #ccc; touch-action: none; } input[type="file"] { margin: 5px; } button { margin-top: 10px; padding: 8px 16px; } </style> </head> <body> <div> <label>Top Image: <input type="file" id="topImage" accept="image/*"></label> <label>Bottom Image: <input type="file" id="bottomImage" accept="image/*"></label> <label>Background (optional): <input type="file" id="bgImage" accept="image/*"></label> </div> <button id="downloadBtn">Flatten & Download PNG</button> <br><br> <canvas id="canvas" width="512" height="512"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const layers = { bg: { img: null, x: 0, y: 0, scale: 1 }, bottom: { img: null, x: 0, y: 0, scale: 1 }, top: { img: null, x: 0, y: 0, scale: 1 } }; let activeLayer = null; let dragging = false; let dragStart = { x: 0, y: 0 }; let lastTouches = []; function loadImage(input, key) { const file = input.files[0]; if (!file) return; const img = new Image(); img.onload = () => { layers[key].img = img; layers[key].scale = 1; layers[key].x = (canvas.width - img.width) / 2; layers[key].y = (canvas.height - img.height) / 2; draw(); }; img.src = URL.createObjectURL(file); } ['topImage', 'bottomImage', 'bgImage'].forEach(id => { document.getElementById(id).addEventListener('change', e => { const key = id.replace('Image', ''); loadImage(e.target, key); }); }); function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ['bg', 'bottom', 'top'].forEach(key => { const layer = layers[key]; if (layer.img) { const w = layer.img.width * layer.scale; const h = layer.img.height * layer.scale; ctx.drawImage(layer.img, layer.x, layer.y, w, h); } }); } function getTouchCenter(touches) { const x = (touches[0].clientX + touches[1].clientX) / 2; const y = (touches[0].clientY + touches[1].clientY) / 2; const rect = canvas.getBoundingClientRect(); return { x: x - rect.left, y: y - rect.top }; } function distance(t1, t2) { return Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); } function getLayerAt(x, y) { const keys = ['top', 'bottom', 'bg']; for (let i = keys.length - 1; i >= 0; i--) { const key = keys[i]; const layer = layers[key]; if (!layer.img) continue; const w = layer.img.width * layer.scale; const h = layer.img.height * layer.scale; if (x >= layer.x && x <= layer.x + w && y >= layer.y && y <= layer.y + h) { return key; } } return null; } canvas.addEventListener('mousedown', e => { const x = e.offsetX; const y = e.offsetY; activeLayer = getLayerAt(x, y); if (activeLayer) { dragging = true; dragStart = { x, y }; } }); canvas.addEventListener('mousemove', e => { if (!dragging || !activeLayer) return; const dx = e.offsetX - dragStart.x; const dy = e.offsetY - dragStart.y; dragStart = { x: e.offsetX, y: e.offsetY }; layers[activeLayer].x += dx; layers[activeLayer].y += dy; draw(); }); canvas.addEventListener('mouseup', () => { dragging = false; }); canvas.addEventListener('wheel', e => { const x = e.offsetX; const y = e.offsetY; const key = getLayerAt(x, y); if (!key) return; e.preventDefault(); const layer = layers[key]; const zoomFactor = 1.05; const scaleChange = e.deltaY < 0 ? zoomFactor : 1 / zoomFactor; const oldScale = layer.scale; layer.scale *= scaleChange; const newScale = layer.scale; const dx = x - layer.x; const dy = y - layer.y; layer.x -= dx * (newScale - oldScale) / oldScale; layer.y -= dy * (newScale - oldScale) / oldScale; draw(); }); canvas.addEventListener('touchstart', e => { e.preventDefault(); lastTouches = [...e.touches]; const touch = e.touches[0]; const rect = canvas.getBoundingClientRect(); const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; activeLayer = getLayerAt(x, y); }, { passive: false }); canvas.addEventListener('touchmove', e => { e.preventDefault(); const touches = [...e.touches]; const rect = canvas.getBoundingClientRect(); const layer = layers[activeLayer]; if (!layer) return; if (touches.length === 1 && lastTouches.length === 1) { const dx = touches[0].clientX - lastTouches[0].clientX; const dy = touches[0].clientY - lastTouches[0].clientY; layer.x += dx; layer.y += dy; } else if (touches.length === 2 && lastTouches.length === 2) { const prevDist = distance(lastTouches[0], lastTouches[1]); const newDist = distance(touches[0], touches[1]); const scaleChange = newDist / prevDist; const center = getTouchCenter(touches); const dx = center.x - layer.x; const dy = center.y - layer.y; const oldScale = layer.scale; layer.scale *= scaleChange; layer.x -= dx * (layer.scale - oldScale) / oldScale; layer.y -= dy * (layer.scale - oldScale) / oldScale; } lastTouches = touches; draw(); }, { passive: false }); canvas.addEventListener('touchend', () => { lastTouches = []; }); document.getElementById('downloadBtn').addEventListener('click', () => { const tempCanvas = document.createElement('canvas'); tempCanvas.width = 512; tempCanvas.height = 512; const tempCtx = tempCanvas.getContext('2d'); ['bg', 'bottom', 'top'].forEach(key => { const layer = layers[key]; if (layer.img) { const w = layer.img.width * layer.scale; const h = layer.img.height * layer.scale; tempCtx.drawImage(layer.img, layer.x, layer.y, w, h); } }); const link = document.createElement('a'); link.download = 'flattened.png'; link.href = tempCanvas.toDataURL('image/png'); link.click(); }); </script> </body> </html> Html source url/file find (find facebook mp4 url) -------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Link Extractor</title> </head> <body> <h1>1) Upload html file</h1> <!-- File Picker --> <input type="file" id="fileInput" /> <!-- Textbox for extension --> <label for="extInput">Extension (e.g. .mp4): </label> <input type="text" id="extInput" value=".mp4" /> <div id="output"></div> <h2>2.) paste url and fix it</h2> <!-- Textbox for raw input --> <textarea id="rawInput" rows="10" cols="80" placeholder="Paste raw text containing URLs here..."></textarea> <br /> <button onclick="processRawText()">Process Raw Text</button> <h2>Decoded URL:</h2> <textarea id="decodedUrlOutput" rows="3" cols="80" readonly></textarea> <script> // When the file is selected document.getElementById('fileInput').addEventListener('change', handleFileSelect, false); // Handle file selection function handleFileSelect(event) { const file = event.target.files[0]; const ext = document.getElementById('extInput').value.trim(); // Get the user-defined extension if (file) { const reader = new FileReader(); // Read file as text reader.onload = function(e) { const fileContent = e.target.result; extractLinks(fileContent, ext); }; reader.readAsText(file); } else { alert("Please upload a valid file."); } } // Extract links from the content based on extension function extractLinks(fileContent, ext) { const links = []; let startIdx = 0; const extLower = ext.toLowerCase(); // Make sure extension is case-insensitive while ((startIdx = fileContent.indexOf(extLower, startIdx)) !== -1) { // Move backwards to find the start of the URL (https) let httpsIdx = fileContent.lastIndexOf('https', startIdx); if (httpsIdx !== -1) { // Extract from https to the first double quote (") let url = fileContent.slice(httpsIdx, fileContent.indexOf('"', httpsIdx)); links.push(url); } startIdx += extLower.length; // Move past the current extension to search for the next one } // Display the results const output = document.getElementById('output'); output.innerHTML = ''; if (links.length > 0) { output.innerHTML = '<h2>Links Found:</h2>'; const ul = document.createElement('ul'); links.forEach(link => { const li = document.createElement('li'); const linkText = document.createElement('span'); linkText.textContent = link; // Create the copy button const copyButton = document.createElement('button'); copyButton.textContent = 'Copy'; copyButton.onclick = () => copyToClipboard(link); li.appendChild(linkText); li.appendChild(copyButton); ul.appendChild(li); }); output.appendChild(ul); } else { output.innerHTML = '<p>No links found with the specified extension.</p>'; } } // Function to copy the link to the clipboard function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { alert('Link copied to clipboard!'); }).catch(err => { alert('Error copying to clipboard: ' + err); }); } // Process the raw input text and fix the URLs function processRawText() { const rawText = document.getElementById('rawInput').value; const fixedUrl = fixUrl(rawText); const output = document.getElementById('output'); output.innerHTML = ''; if (fixedUrl) { output.innerHTML = `<h2>Fixed URL:</h2> <p><a href="${fixedUrl}" target="_blank">${fixedUrl}</a></p>`; // Display the fixed URL in the decoded URL output area document.getElementById('decodedUrlOutput').value = fixedUrl; } else { output.innerHTML = '<p>No valid URL found in the raw text.</p>'; } } // Fix and decode the URL function fixUrl(rawText) { // Regular expression to find URLs starting with https:// const regex = /https:\/\/[^\s"]+/g; let match = regex.exec(rawText); if (match) { let url = match[0]; // Decode escape sequences like \/ -> / url = url.replace(/\\\//g, '/'); // Decode percent-encoded characters (e.g., \u00253D -> =) url = decodeURIComponent(url); // Check if it's a valid URL and return it try { new URL(url); // Check if the URL is valid return url; } catch (e) { console.error('Invalid URL:', url); return null; } } return null; } </script> </body> </html> Anti PayWall(s) ----------------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Smart URL Proxy Router</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: #333; } .container { background-color: #f5f5f5; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } input[type="url"] { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; font-size: 16px; } button { background-color: #4CAF50; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-right: 10px; margin-bottom: 10px; } button:hover { background-color: #45a049; } button.secondary { background-color: #2196F3; } button.secondary:hover { background-color: #0b7dda; } select { padding: 8px; border-radius: 4px; border: 1px solid #ddd; font-size: 16px; margin: 10px 0; width: 100%; } .bookmarklet { background-color: #e9f7ef; padding: 15px; border-radius: 4px; margin-top: 10px; word-break: break-all; cursor: pointer; font-family: monospace; } .bookmarklet:hover { background-color: #d4edda; } .instructions { margin-top: 20px; font-size: 14px; color: #555; } .service-info { margin-top: 15px; padding: 10px; background-color: #e7f3fe; border-radius: 4px; display: none; } .active { font-weight: bold; color: #2c7be5; } .section { margin-top: 25px; padding-top: 15px; border-top: 1px solid #eee; } h2 { color: #2c7be5; margin-bottom: 10px; } </style> </head> <body> <div class="container"> <h1>Smart URL Proxy Router</h1> <p>Open any URL through the most appropriate proxy service:</p> <select id="serviceSelector"> <option value="auto">Auto-Select (Smart Proxy)</option> <option value="freedium">Freedium</option> <option value="12ft">12ft.io</option> <option value="archive">Archive.today</option> <option value="libmedium">LibMedium</option> <option value="scribe">Scribe.rip</option> <option value="threadreader">ThreadReaderApp</option> <option value="googlecache">Google Cache</option> </select> <div id="serviceInfo" class="service-info"> <!-- Service information will be displayed here --> </div> <input type="url" id="urlInput" placeholder="https://example.com/article" autofocus> <button onclick="openProxy()">Open via Selected Proxy</button> <button class="secondary" onclick="rotateService()">Rotate to Next Service</button> <div class="section"> <h2>Smart Universal Bookmarklet</h2> <p>This bookmarklet automatically selects the best proxy based on the current website:</p> <div id="smartBookmarklet" class="bookmarklet" onclick="copyBookmarklet('smart')" title="Click to copy"> <!-- Smart bookmarklet code will be inserted here --> </div> <p class="instructions">Drag this link to your bookmarks bar to create a smart one-click proxy button that works everywhere.</p> </div> <div class="section"> <h2>Specific Service Bookmarklet</h2> <p>Bookmarklet for currently selected service:</p> <div id="bookmarkletCode" class="bookmarklet" onclick="copyBookmarklet('current')" title="Click to copy"> <!-- Current service bookmarklet will be inserted here --> </div> <p class="instructions">Drag this link to your bookmarks bar to always use the selected proxy service.</p> </div> </div> <script> // Proxy services configuration const services = [ { id: "auto", name: "Auto-Select", url: "", info: "Smart proxy that automatically selects the best service for the current website." }, { id: "freedium", name: "Freedium", url: "https://freedium.cfd/", info: "Bypasses Medium paywall by showing cached versions of articles.", domains: ["medium.com", "towardsdatascience.com"] }, { id: "12ft", name: "12ft.io", url: "https://12ft.io/", info: "General purpose paywall remover that works on many sites including Medium, Bloomberg, etc.", domains: ["bloomberg.com", "washingtonpost.com", "ft.com"] }, { id: "archive", name: "Archive.today", url: "https://archive.today/?run=1&url=", info: "Creates permanent archived copies of web pages, bypassing paywalls.", domains: ["nytimes.com", "wsj.com", "economist.com"] }, { id: "libmedium", name: "LibMedium", url: "https://libmedium.batsense.net/", info: "Alternative Medium proxy focused on privacy and accessibility.", domains: ["medium.com"] }, { id: "scribe", name: "Scribe.rip", url: "https://scribe.rip/", info: "Another Medium-specific proxy that reformats articles for better readability.", domains: ["medium.com"] }, { id: "threadreader", name: "ThreadReader", url: "https://threadreaderapp.com/thread/", info: "For unrolling Twitter threads (only works with Twitter URLs).", domains: ["twitter.com", "x.com"] }, { id: "googlecache", name: "Google Cache", url: "https://webcache.googleusercontent.com/search?q=cache:", info: "Google's cached version of pages (when available).", domains: ["*"] } ]; let currentServiceIndex = 1; // Start with Freedium as default // Initialize the page function init() { updateServiceInfo(); updateBookmarklet(); updateSmartBookmarklet(); } // Open URL with selected proxy service function openProxy() { const url = document.getElementById('urlInput').value.trim(); if (!url) { alert('Please enter a URL'); return; } try { new URL(url); } catch (e) { alert('Please enter a valid URL (include http:// or https://)'); return; } const serviceId = document.getElementById('serviceSelector').value; let service; if (serviceId === "auto") { service = autoSelectService(url); } else { service = services.find(s => s.id === serviceId); } let proxyUrl; // Special handling for certain services if (service.id === "threadreader") { const tweetId = extractTweetId(url); if (tweetId) { proxyUrl = `${service.url}${tweetId}`; } else { alert("ThreadReader only works with Twitter URLs"); return; } } else { proxyUrl = `${service.url}${url}`; } window.open(proxyUrl, '_blank'); } // Auto-select the best service based on URL domain function autoSelectService(url) { const urlObj = new URL(url); const hostname = urlObj.hostname.replace('www.', ''); // Check for specific domain matches first for (const service of services) { if (service.domains) { for (const domain of service.domains) { if (hostname === domain || (domain === '*' && service.id !== 'auto') || hostname.endsWith('.' + domain)) { return service; } } } } // Default to 12ft.io for general paywalls, then archive.today return services.find(s => s.id === "12ft") || services.find(s => s.id === "archive"); } // Extract tweet ID from Twitter URL function extractTweetId(url) { const twitterRegex = /twitter\.com\/\w+\/status\/(\d+)/; const match = url.match(twitterRegex); return match ? match[1] : null; } // Rotate to next service function rotateService() { currentServiceIndex = (currentServiceIndex + 1) % services.length; if (services[currentServiceIndex].id === "auto") { currentServiceIndex = (currentServiceIndex + 1) % services.length; } document.getElementById('serviceSelector').value = services[currentServiceIndex].id; updateServiceInfo(); updateBookmarklet(); } // Update service info display function updateServiceInfo() { const serviceId = document.getElementById('serviceSelector').value; const service = serviceId === "auto" ? services[0] : services.find(s => s.id === serviceId); const infoDiv = document.getElementById('serviceInfo'); infoDiv.innerHTML = `<strong>${service.name}</strong>: ${service.info}`; infoDiv.style.display = 'block'; } // Update bookmarklet code for current service function updateBookmarklet() { const serviceId = document.getElementById('serviceSelector').value; const service = services.find(s => s.id === serviceId); let bookmarkletCode; if (service.id === "threadreader") { bookmarkletCode = `javascript:(function(){const tweetId=location.href.match(/twitter\\.com\\/\\w+\\/status\\/(\\d+)/);if(tweetId){window.open('${service.url}'+tweetId[1]);}else{alert('This only works on Twitter thread pages');}})();`; } else if (service.id === "auto") { bookmarkletCode = document.getElementById('smartBookmarklet').textContent; } else { bookmarkletCode = `javascript:(function(){window.open('${service.url}'+location.href);})();`; } document.getElementById('bookmarkletCode').textContent = bookmarkletCode; } // Create the smart universal bookmarklet function updateSmartBookmarklet() { const smartBookmarklet = `javascript:(function(){ const url = location.href; const hostname = new URL(url).hostname.replace('www.', ''); // Service matching logic ${services.filter(s => s.id !== 'auto').map(service => { if (!service.domains) return ''; const domainChecks = service.domains.map(domain => { if (domain === '*') return 'true'; return `hostname === '${domain}' || hostname.endsWith('.${domain}')`; }).join(' || '); return `if (${domainChecks}) { window.open('${service.url}' + url); return; }`; }).join('\n')} // Default services window.open('https://12ft.io/' + url); })();`; document.getElementById('smartBookmarklet').textContent = smartBookmarklet; } // Copy bookmarklet to clipboard function copyBookmarklet(type) { const elementId = type === 'smart' ? 'smartBookmarklet' : 'bookmarkletCode'; const bookmarklet = document.getElementById(elementId).textContent; navigator.clipboard.writeText(bookmarklet).then(() => { alert('Bookmarklet copied to clipboard! Drag it to your bookmarks bar.'); }).catch(err => { console.error('Failed to copy: ', err); prompt("Copy this bookmarklet code:", bookmarklet); }); } // Event listeners document.getElementById('serviceSelector').addEventListener('change', function() { updateServiceInfo(); updateBookmarklet(); }); document.getElementById('urlInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') { openProxy(); } }); // Initialize on load window.onload = init; </script> </body> </html> MFM Markdown (Akkoma/Misskey) --------------------------- <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>MFM Builder — Interactive</title> <style> :root{--accent:#7c3aed;--muted:#6b7280;--bg:#0b1220;--card:#071022;--glass:rgba(255,255,255,0.02)} html,body{height:100%;margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Arial;background:linear-gradient(180deg,#041022,#071224);color:#e6eef8} .wrap{max-width:1100px;margin:20px auto;padding:18px} h1{margin:0 0 8px;font-size:18px;color:var(--accent)} p.lead{margin:0 0 16px;color:var(--muted);font-size:13px} .layout{display:grid;grid-template-columns:1fr 420px;gap:14px} .card{background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0.01));border:1px solid rgba(255,255,255,0.03);padding:12px;border-radius:10px;box-shadow:0 6px 20px rgba(2,6,23,0.6)} label.block{display:block;margin-bottom:6px;font-size:13px;color:var(--muted)} textarea#input{text-rendering:optimizeLegibility;width:100%;min-height:120px;border-radius:8px;padding:10px;background:transparent;border:1px solid rgba(255,255,255,0.03);color:#dbeafe;font-family:inherit;resize:vertical} .toolbar{display:flex;flex-direction:column;gap:8px} .group{display:flex;flex-wrap:wrap;gap:8px} button.btn{appearance:none;border:0;padding:8px 10px;border-radius:8px;background:rgba(255,255,255,0.02);color:#dbeafe;cursor:pointer;font-weight:600} button.btn.alt{background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--muted);font-weight:600} .small{font-size:12px;color:var(--muted)} input[type="text"], input[type="number"], select{padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.03);background:transparent;color:#dbeafe} .row{display:flex;gap:8px;align-items:center} .preview-area{display:flex;flex-direction:column;gap:8px} textarea#mfm{height:120px;width:100%;border-radius:8px;padding:10px;background:transparent;border:1px solid rgba(255,255,255,0.03);color:#dbeafe;font-family:ui-monospace,Menlo,monospace} .actions{display:flex;gap:8px;justify-content:space-between;align-items:center} .muted{color:var(--muted);font-size:12px} footer{margin-top:12px;color:var(--muted);font-size:12px} @media (max-width:980px){.layout{grid-template-columns:1fr;}} /* MFM preview specific styles */ .mfm-preview blockquote{border-left:3px solid var(--accent);padding-left:12px;margin:8px 0;opacity:0.8} .mfm-preview pre{background:rgba(255,255,255,0.05);padding:10px;border-radius:6px;overflow-x:auto} .mfm-preview code{font-family:ui-monospace,Menlo,monospace;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px;font-size:0.9em} .mfm-preview .mfm-spin{display:inline-block;animation:mfm-spin 1.5s linear infinite} .mfm-preview .mfm-spin.x{display:inline-block;animation:mfm-spin-x 1.5s linear infinite} .mfm-preview .mfm-spin.y{display:inline-block;animation:mfm-spin-y 1.5s linear infinite} .mfm-preview .mfm-jump{display:inline-block;animation:mfm-jump 0.75s linear infinite} .mfm-preview .mfm-bounce{display:inline-block;animation:mfm-bounce 0.75s linear infinite} .mfm-preview .mfm-shake{display:inline-block;animation:mfm-shake 0.5s ease infinite} .mfm-preview .mfm-twitch{display:inline-block;animation:mfm-twitch 0.5s ease infinite} .mfm-preview .mfm-rainbow{animation:mfm-rainbow 3s linear infinite} .mfm-preview .mfm-sparkle{position:relative;display:inline-block} .mfm-preview .mfm-flip{transform:scale(-1,-1);display:inline-block} .mfm-preview .mfm-flip.h{transform:scale(-1,1);display:inline-block} .mfm-preview .mfm-flip.v{transform:scale(1,-1);display:inline-block} .mfm-preview .mfm-x2{transform:scale(2);display:inline-block;transform-origin:left center} .mfm-preview .mfm-x3{transform:scale(3);display:inline-block;transform-origin:left center} .mfm-preview .mfm-x4{transform:scale(4);display:inline-block;transform-origin:left center} @keyframes mfm-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} @keyframes mfm-spin-x{0%{transform:rotateX(0deg)}100%{transform:rotateX(360deg)}} @keyframes mfm-spin-y{0%{transform:rotateY(0deg)}100%{transform:rotateY(360deg)}} @keyframes mfm-jump{0%,100%{transform:translateY(0)}50%{transform:translateY(-0.5em)}} @keyframes mfm-bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-0.25em)}} @keyframes mfm-shake{0%{transform:translateX(0)}25%{transform:translateX(-5px)}50%{transform:translateX(0)}75%{transform:translateX(5px)}100%{transform:translateX(0)}} @keyframes mfm-twitch{0%{transform:translateX(0) rotate(0deg)}5%{transform:translateX(0) rotate(0deg)}6%{transform:translateX(0) rotate(-3deg)}10%{transform:translateX(0) rotate(3deg)}15%{transform:translateX(0) rotate(0deg)}} @keyframes mfm-rainbow{0%{color:#ff0000}14%{color:#ff7f00}28%{color:#ffff00}42%{color:#00ff00}56%{color:#0000ff}70%{color:#4b0082}84%{color:#9400d3}100%{color:#ff0000}} </style> </head> <body> <div class="wrap"> <h1>MFM Builder</h1> <p class="lead">Type text, apply modifiers from groups, preview MFM source, then copy to clipboard.</p> <div class="layout"> <div class="card"> <label class="block">Input text (you can select part of text and apply modifiers to selection)</label> <textarea id="input" placeholder="Type here — e.g. MisskeyでFediverse 🍮"></textarea> <div style="height:10px"></div> <div class="toolbar"> <div> <div class="small" style="margin-bottom:6px">Formatting</div> <div class="group" id="format-group"> <button class="btn" data-action="wrap" data-value="**{t}**">Bold</button> <button class="btn" data-action="wrap" data-value="~~{t}~~">Strikethrough</button> <button class="btn" data-action="wrap" data-value="`{t}`">Inline code</button> <button class="btn" data-action="wrap" data-value="> {t}">Quote</button> <button class="btn" data-action="wrap" data-value="<small>{t}</small>">Small</button> <button class="btn" data-action="wrap" data-value="<center>{t}</center>">Center</button> <button class="btn" data-action="wrap" data-value=":$:{t}:$:">CustomEmoji (name)</button> </div> </div> <div> <div class="small" style="margin-bottom:6px">Transforms / Layout</div> <div class="group" id="transform-group"> <button class="btn" data-action="wrap" data-value="$[flip {t}]">Flip</button> <button class="btn" data-action="wrap" data-value="$[flip.h {t}]">Flip H</button> <button class="btn" data-action="wrap" data-value="$[flip.v {t}]">Flip V</button> <button class="btn" data-action="wrap" data-value="$[rotate.deg={deg} {t}]">Rotate</button> <button class="btn" data-action="wrap" data-value="$[position.x={px} {t}]">Shift X</button> <button class="btn" data-action="wrap" data-value="$[position.y={py} {t}]">Shift Y</button> <button class="btn" data-action="wrap" data-value="$[scale.x={sx},y={sy} {t}]">Scale</button> </div> <div class="row" style="margin-top:6px;gap:10px"> <input type="number" id="deg" placeholder="deg" style="width:86px"/> <input type="text" id="px" placeholder="x offset (e.g. 1.5)" style="width:120px"/> <input type="text" id="py" placeholder="y offset" style="width:120px"/> <input type="text" id="sx" placeholder="scale x" style="width:86px"/> <input type="text" id="sy" placeholder="scale y" style="width:86px"/> </div> </div> <div> <div class="small" style="margin-bottom:6px">Animations / Effects</div> <div class="group" id="anim-group"> <button class="btn" data-action="wrap" data-value="$[jelly {t}]">Jelly</button> <button class="btn" data-action="wrap" data-value="$[tada {t}]">Tada</button> <button class="btn" data-action="wrap" data-value="$[jump {t}]">Jump</button> <button class="btn" data-action="wrap" data-value="$[bounce {t}]">Bounce</button> <button class="btn" data-action="wrap" data-value="$[spin {t}]">Spin</button> <button class="btn" data-action="wrap" data-value="$[spin.x {t}]">Spin X</button> <button class="btn" data-action="wrap" data-value="$[spin.y {t}]">Spin Y</button> <button class="btn" data-action="wrap" data-value="$[shake {t}]">Shake</button> <button class="btn" data-action="wrap" data-value="$[twitch {t}]">Twitch</button> <button class="btn" data-action="wrap" data-value="$[sparkle {t}]">Sparkle</button> </div> <div style="margin-top:6px" class="group"> <button class="btn" data-action="wrap" data-value="$[rainbow {t}]">Rainbow</button> <button class="btn" data-action="wrap" data-value="$[x2 {t}]">$[x2]</button> <button class="btn" data-action="wrap" data-value="$[x3 {t}]">$[x3]</button> <button class="btn" data-action="wrap" data-value="$[x4 {t}]">$[x4]</button> </div> </div> <div> <div class="small" style="margin-bottom:6px">Decorations / Border / Color</div> <div class="group"> <button class="btn" data-action="wrap" data-value="$[border.style=solid,width=4 {t}]">Border solid</button> <button class="btn" data-action="wrap" data-value="$[border.color={bcol} {t}]">Border color</button> <button class="btn" data-action="wrap" data-value="$[fg.color={fcol} {t}]">FG color</button> <button class="btn" data-action="wrap" data-value="$[bg.color={bkg} {t}]">BG color</button> <button class="btn" data-action="wrap" data-value="$[blur {t}]">Blur</button> </div> <div class="row" style="margin-top:6px"> <input type="text" id="bcol" placeholder="border color (hex like f00)" style="width:120px"/> <input type="text" id="fcol" placeholder="fg color (hex)" style="width:120px"/> <input type="text" id="bkg" placeholder="bg color (hex)" style="width:120px"/> </div> </div> <div> <div class="small" style="margin-bottom:6px">Advanced / Others</div> <div class="group"> <button class="btn" data-action="wrap" data-value="$[ruby {t} {ruby}]">$[ruby]</button> <button class="btn" data-action="wrap" data-value="` ```{lang}\n{t}\n``` `">Code block</button> <button class="btn" data-action="wrap" data-value="[{text}]({url})">Link</button> <button class="btn" data-action="wrap" data-value="?[{text}]({url})">Hidden preview link</button> </div> <div class="row" style="margin-top:6px"> <input type="text" id="ruby" placeholder="ruby text" style="width:180px"/> <input type="text" id="lang" placeholder="lang id (e.g. ais)" style="width:120px"/> <input type="text" id="url" placeholder="https://..." style="width:180px"/> </div> </div> </div> </div> <div class="card preview-area"> <div> <label class="block">Generated MFM source</label> <textarea id="mfm" readonly placeholder="MFM result will appear here"></textarea> </div> <div class="actions"> <div style="display:flex;gap:8px"> <button id="copy" class="btn">Copy MFM</button> <button id="reset" class="btn alt">Reset</button> <button id="wrap-selection" class="btn alt">Wrap selection with custom</button> </div> <div style="text-align:right"> <div class="muted" id="status">Ready</div> </div> </div> <div style="margin-top:10px"> <label class="block">Preview (best-effort plain preview — actual Misskey client renders MFM differently)</label> <div id="visualPreview" class="mfm-preview" style="min-height:80px;padding:10px;border-radius:8px;background:var(--glass);border:1px solid rgba(255,255,255,0.03);color:#dbeafe;font-size:14px"></div> </div> <div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:center"> <div class="small">Auto-update MFM:</div> <label class="small"><input type="checkbox" id="auto" checked/> live</label> </div> </div> </div> <footer>Single-file. Works offline. Uses navigator.clipboard with fallback.</footer> </div> <script> /* Full client-side MFM builder logic. - Buttons with data-action="wrap" and data-value use templates where {t} is text, and other placeholders (deg, px, py, sx, sy, bcol, fcol, bkg, ruby, lang, url, text) are replaced with inputs from the toolbar. - If selection exists in #input, wrap selection; otherwise wrap entire input. - Live updates MFM output and a simple visual preview (plain text with some replacements). */ /* helpers */ function el(q, root=document) { return root.querySelector(q); } function els(q, root=document) { return Array.from(root.querySelectorAll(q)); } function setStatus(txt, ok=true) { const s = el('#status'); s.textContent = txt; s.style.color = ok ? '' : '#ff9b9b'; } /* get values */ function getVal(id){ const e = el('#'+id); return e ? e.value : ''; } /* create wrapper with template */ function buildWrapper(template, text) { if (!template) return text; // gather replacements const repl = { t: text, deg: getVal('deg'), px: getVal('px'), py: getVal('py'), sx: getVal('sx') || '2', sy: getVal('sy') || getVal('sx') || '2', bcol: getVal('bcol'), fcol: getVal('fcol'), bkg: getVal('bkg'), ruby: getVal('ruby'), lang: getVal('lang'), url: getVal('url'), text: text }; // For code block template that used backticks wrappers we may have weird spacing; allow {lang} insertion. let out = template; Object.keys(repl).forEach(k => { const v = repl[k] === undefined ? '' : repl[k]; // escape braces in replacement value for safe replacement out = out.split('{' + k + '}').join(v); }); return out; } /* apply wrapper to selection or whole input */ function applyWrapperToInput(template) { const ta = el('#input'); ta.focus(); const start = ta.selectionStart; const end = ta.selectionEnd; const before = ta.value.slice(0, start); const sel = (start !== end) ? ta.value.slice(start, end) : ta.value.slice(0); // if no selection, will wrap whole text (from 0) // If no selection and there is text, wrap entire text; if empty, insert placeholder const textToWrap = sel.length ? sel : 'your text'; const wrapped = buildWrapper(template, textToWrap); if (start !== end) { ta.value = before + wrapped + ta.value.slice(end); // reselect newly wrapped content ta.setSelectionRange(before.length, before.length + wrapped.length); } else { // no selection: replace entire or insert at cursor if (ta.value.length) { // wrap entire value ta.value = wrapped; ta.setSelectionRange(0, wrapped.length); } else { // insert placeholder wrapped at cursor ta.value = wrapped; ta.setSelectionRange(0, wrapped.length); } } updateAll(); } /* take a template that may be a link template like "[{text}]({url})" - support special flow: if template contains "[{text}]" then the tool expects 'text' and 'url' inputs. */ function handleWrapButton(btn) { const template = btn.getAttribute('data-value'); applyWrapperToInput(template); } /* Custom MFM parser for preview */ function parseMFM(text) { if (!text) return ''; // First parse code blocks to prevent interpreting their contents as MFM const codeBlockRegex = /```([a-z]*)\n([\s\S]*?)\n```/g; text = text.replace(codeBlockRegex, (match, lang, code) => { return `<pre><code class="language-${lang}">${escapeHTML(code)}</code></pre>`; }); // Then parse inline code text = text.replace(/`([^`]+)`/g, (match, code) => { return `<code>${escapeHTML(code)}</code>`; }); // Parse MFM functions ($[...]) const mfmFuncRegex = /\$\[([a-z.]+)(?:=([^\]]+))? (.*?)\]/g; text = text.replace(mfmFuncRegex, (match, func, params, content) => { content = parseMFM(content); // Recursively parse nested content switch(func) { case 'flip': return `<span class="mfm-flip">${content}</span>`; case 'flip.h': return `<span class="mfm-flip h">${content}</span>`; case 'flip.v': return `<span class="mfm-flip v">${content}</span>`; case 'spin': return `<span class="mfm-spin">${content}</span>`; case 'spin.x': return `<span class="mfm-spin x">${content}</span>`; case 'spin.y': return `<span class="mfm-spin y">${content}</span>`; case 'jump': return `<span class="mfm-jump">${content}</span>`; case 'bounce': return `<span class="mfm-bounce">${content}</span>`; case 'shake': return `<span class="mfm-shake">${content}</span>`; case 'twitch': return `<span class="mfm-twitch">${content}</span>`; case 'rainbow': return `<span class="mfm-rainbow">${content}</span>`; case 'x2': return `<span class="mfm-x2">${content}</span>`; case 'x3': return `<span class="mfm-x3">${content}</span>`; case 'x4': return `<span class="mfm-x4">${content}</span>`; case 'position.x': case 'position.y': // Position is tricky to preview - just show content return content; case 'rotate.deg': const deg = params ? params.split('=')[1] : '0'; return `<span style="display:inline-block;transform:rotate(${deg}deg)">${content}</span>`; case 'scale.x': case 'scale.y': // Scale params might be x=2,y=3 or just x=2 let scaleX = '1', scaleY = '1'; if (params) { const parts = params.split(','); parts.forEach(part => { const [key, value] = part.split('='); if (key === 'x') scaleX = value; if (key === 'y') scaleY = value; }); } return `<span style="display:inline-block;transform:scale(${scaleX},${scaleY})">${content}</span>`; case 'fg.color': const fgColor = params || 'inherit'; return `<span style="color:#${fgColor}">${content}</span>`; case 'bg.color': const bgColor = params || 'transparent'; return `<span style="background-color:#${bgColor};padding:0 2px;border-radius:3px">${content}</span>`; case 'border.color': const borderColor = params || 'currentColor'; return `<span style="border:1px solid #${borderColor};padding:0 2px;border-radius:3px">${content}</span>`; case 'border.style': // Parse border style params (style=solid,width=4) let borderStyle = 'solid', borderWidth = '1px'; if (params) { const parts = params.split(','); parts.forEach(part => { const [key, value] = part.split('='); if (key === 'style') borderStyle = value; if (key === 'width') borderWidth = value + 'px'; }); } return `<span style="border:${borderWidth} ${borderStyle} currentColor;padding:0 2px;border-radius:3px">${content}</span>`; case 'ruby': // Ruby text is in the content part (content is "base ruby") const parts = content.split(/\s+/); if (parts.length >= 2) { const base = parts.slice(0, -1).join(' '); const ruby = parts[parts.length - 1]; return `<ruby>${base}<rt>${ruby}</rt></ruby>`; } return content; default: return content; } }); // Parse markdown-like syntax text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); text = text.replace(/~~(.*?)~~/g, '<del>$1</del>'); text = text.replace(/> (.*)/g, '<blockquote>$1</blockquote>'); text = text.replace(/<small>(.*?)<\/small>/g, '<small>$1</small>'); text = text.replace(/<center>(.*?)<\/center>/g, '<div style="text-align:center">$1</div>'); // Parse links text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'); text = text.replace(/\?\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" style="text-decoration:none;color:inherit">$1</a>'); // Parse custom emoji text = text.replace(/:(\w+):/g, '<span style="display:inline-block;background:rgba(255,255,255,0.1);border-radius:4px;padding:0 2px">:$1:</span>'); return text; } function escapeHTML(str) { return str.replace(/[&<>'"]/g, tag => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[tag])); } /* update MFM textarea from input */ function updateAll() { const input = el('#input').value; // For now the "generated MFM source" is the raw input (modifiers are applied by buttons) el('#mfm').value = input; // Use our custom MFM parser for the preview el('#visualPreview').innerHTML = parseMFM(input); } /* copy to clipboard with fallback */ async function copyText(text) { if (!text) return false; if (navigator.clipboard && navigator.clipboard.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch(e) {} } try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } catch(e) { return false; } } /* wire buttons */ document.addEventListener('click', (ev) => { const btn = ev.target.closest('button[data-action="wrap"]'); if (btn) { handleWrapButton(btn); setStatus('Wrapped'); return; } }); els('button.btn').forEach(b => { // non-wrap buttons already handled using data-action attribute in event delegation }); /* copy button */ el('#copy').addEventListener('click', async () => { const txt = el('#mfm').value; const ok = await copyText(txt); setStatus(ok ? 'Copied to clipboard' : 'Copy failed', ok); }); /* reset */ el('#reset').addEventListener('click', () => { el('#input').value = ''; el('#mfm').value = ''; el('#visualPreview').innerHTML = ''; setStatus('Reset'); }); /* wrap-selection: prompt for a custom template and wrap selection or entire text */ el('#wrap-selection').addEventListener('click', () => { const tpl = prompt('Enter wrapper template with {t} as placeholder (e.g. $[spin {t}])','$[spin {t}]'); if (!tpl) return; applyWrapperToInput(tpl); setStatus('Applied custom wrapper'); }); /* live updates */ el('#input').addEventListener('input', () => { if (el('#auto').checked) updateAll(); }); els('input').forEach(i=>i.addEventListener('change', () => { if (el('#auto').checked) updateAll(); })); /* initialize */ updateAll(); setStatus('Ready'); /* Accessibility: keyboard shortcuts */ document.addEventListener('keydown', (ev) => { if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'b') { // ctrl/cmd+B toggle bold on selection ev.preventDefault(); applyWrapperToInput('**{t}**'); setStatus('Bold applied'); } }); </script> </body> </html> FFMPEG Video compressor / resizer ---------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Video Compressor with ffmpeg.wasm</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: auto; padding: 20px; } input[type=range] { width: 100%; } label { display: block; margin-top: 10px; } video { max-width: 100%; margin-top: 10px; } pre#debugLog { background: #111; color: #0f0; padding: 10px; overflow: auto; max-height: 200px; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; } #progress, #ffmpegProgress { width: 100%; height: 16px; } button { margin-top: 10px; } #message { margin-top: 10px; font-weight: bold; } .error { color: red; } .success { color: green; } .tab { overflow: hidden; border: 1px solid #ccc; background-color: #f1f1f1; border-radius: 4px 4px 0 0; } .tab button { background-color: inherit; float: left; border: none; outline: none; cursor: pointer; padding: 10px 16px; transition: 0.3s; margin-top: 0; } .tab button:hover { background-color: #ddd; } .tab button.active { background-color: #ccc; } .tabcontent { display: none; padding: 15px; border: 1px solid #ccc; border-top: none; border-radius: 0 0 4px 4px; } .tabcontent.active { display: block; } #fileInfo { margin: 10px 0; font-style: italic; } .mode-selector { margin: 15px 0; } .mode-options { padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-top: 10px; } .hidden { display: none; } .comparison { display: flex; justify-content: space-between; margin: 20px 0; } .video-container { width: 48%; text-align: center; } .audio-options { margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px; background: #f8f8f8; } .two-column { display: flex; justify-content: space-between; } .column { width: 48%; } </style> </head> <body> <h2>Video Compressor with ffmpeg.wasm</h2> <div class="tab"> <button class="tablinks active" onclick="openTab(event, 'urlTab')">From URL</button> <button class="tablinks" onclick="openTab(event, 'uploadTab')">Upload File</button> </div> <div id="urlTab" class="tabcontent active"> <input type="text" id="videoUrl" placeholder="Enter video URL" size="60" /> <label><input type="checkbox" id="bypassCors" /> Bypass CORS via proxy</label> <button id="loadVideo">Load Video</button> </div> <div id="uploadTab" class="tabcontent"> <input type="file" id="fileUpload" accept="video/*" /> <div id="fileInfo"></div> </div> <button id="loadFFmpegBtn">Load ffmpeg.wasm</button> <div id="message"></div> <div class="comparison"> <div class="video-container"> <h3>Original Video</h3> <video id="inputVideo" controls></video> <div id="originalInfo"></div> </div> <div class="video-container"> <h3>Compressed Video</h3> <video id="outputVideo" controls></video> <div id="compressedInfo"></div> </div> </div> <div class="mode-selector"> <label><strong>Compression Mode:</strong></label><br> <label><input type="radio" name="mode" value="resolution" checked onclick="toggleMode()" /> Resolution (Width x Height)</label><br> <label><input type="radio" name="mode" value="filesize" onclick="toggleMode()" /> Target Filesize</label> </div> <div class="two-column"> <div class="column"> <div id="resolutionOptions" class="mode-options"> <label>Width: <input type="number" id="widthInput" min="64" step="1" /> px</label> <label>Height: <input type="number" id="heightInput" min="64" step="1" /> px</label> <label> Maintain Aspect Ratio: <input type="checkbox" id="keepAspect" checked /> <span id="aspectRatio"></span> </label> <label> Video Quality (CRF): <input type="range" id="qualitySlider" min="0" max="51" value="28" /> <span id="qualityValue">28</span> (Lower = better quality, 23-28 is recommended) </label> </div> <div id="filesizeOptions" class="mode-options hidden"> <label>Target Filesize: <input type="number" id="targetSize" min="0.1" step="0.1" value="5" /> MB</label> <label> Video Quality Preset: <select id="filesizePreset"> <option value="ultrafast">Ultrafast (fastest, larger files)</option> <option value="superfast">Superfast</option> <option value="veryfast">Veryfast</option> <option value="faster">Faster</option> <option value="fast">Fast</option> <option value="medium" selected>Medium (good balance)</option> <option value="slow">Slow</option> <option value="slower">Slower</option> <option value="veryslow">Veryslow (slowest, smallest files)</option> </select> </label> <label> Video/Audio Allocation: <select id="bitrateAllocation"> <option value="90/10">90% Video / 10% Audio</option> <option value="80/20">80% Video / 20% Audio</option> <option value="70/30">70% Video / 30% Audio</option> <option value="60/40">60% Video / 40% Audio</option> </select> </label> </div> </div> <div class="column"> <div class="audio-options"> <h4>Audio Settings</h4> <label> Audio Codec: <select id="audioCodec"> <option value="aac">AAC (recommended)</option> <option value="libmp3lame">MP3</option> <option value="copy">Keep Original</option> <option value="none">No Audio</option> </select> </label> <label> Audio Bitrate: <select id="audioBitrate"> <option value="320k">320 kbps (high quality)</option> <option value="256k">256 kbps</option> <option value="192k">192 kbps</option> <option value="160k">160 kbps</option> <option value="128k" selected>128 kbps (good balance)</option> <option value="96k">96 kbps</option> <option value="64k">64 kbps (low quality)</option> <option value="32k">32 kbps (very low quality)</option> </select> </label> <label> Audio Channels: <select id="audioChannels"> <option value="2">Stereo (2 channels)</option> <option value="1">Mono (1 channel)</option> <option value="copy">Keep Original</option> </select> </label> <label> Sample Rate: <select id="audioSampleRate"> <option value="44100">44.1 kHz (CD quality)</option> <option value="48000">48 kHz</option> <option value="22050">22.05 kHz</option> <option value="copy">Keep Original</option> </select> </label> </div> </div> </div> <button id="compressButton" disabled>Compress Video</button> <progress id="progress" value="0" max="100"></progress> <h3>Debug Log</h3> <pre id="debugLog"></pre> <script src="https://unpkg.com/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js"></script> <script src="https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js"></script> <script> const logBox = document.getElementById('debugLog'); const message = document.getElementById('message'); const inputVideo = document.getElementById('inputVideo'); const outputVideo = document.getElementById('outputVideo'); const progress = document.getElementById('progress'); const loadFFmpegBtn = document.getElementById('loadFFmpegBtn'); const compressButton = document.getElementById('compressButton'); const fileUpload = document.getElementById('fileUpload'); const fileInfo = document.getElementById('fileInfo'); const originalInfo = document.getElementById('originalInfo'); const compressedInfo = document.getElementById('compressedInfo'); const qualitySlider = document.getElementById('qualitySlider'); const qualityValue = document.getElementById('qualityValue'); const widthInput = document.getElementById('widthInput'); const heightInput = document.getElementById('heightInput'); const keepAspect = document.getElementById('keepAspect'); const aspectRatio = document.getElementById('aspectRatio'); const targetSize = document.getElementById('targetSize'); const filesizePreset = document.getElementById('filesizePreset'); const bitrateAllocation = document.getElementById('bitrateAllocation'); const audioCodec = document.getElementById('audioCodec'); const audioBitrate = document.getElementById('audioBitrate'); const audioChannels = document.getElementById('audioChannels'); const audioSampleRate = document.getElementById('audioSampleRate'); // Enhanced console logging ['log', 'error', 'warn', 'info'].forEach(method => { const original = console[method]; console[method] = function(...args) { const msg = args.map(a => { try { return (typeof a === 'object') ? JSON.stringify(a, null, 2) : String(a); } catch { return String(a); } }).join(' '); const time = new Date().toLocaleTimeString(); logBox.textContent += `[${time}] ${method.toUpperCase()}: ${msg}\n`; logBox.scrollTop = logBox.scrollHeight; original.apply(console, args); }; }); const { createFFmpeg, fetchFile } = FFmpeg; const ffmpeg = createFFmpeg({ log: true, corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js' }); let videoBlob = null; let videoDuration = 0; let originalWidth = 0; let originalHeight = 0; let originalSize = 0; let ffmpegLoaded = false; function showMessage(text, isError = false) { message.textContent = text; message.className = isError ? 'error' : 'success'; } function toggleMode() { const mode = document.querySelector('input[name="mode"]:checked').value; document.getElementById('resolutionOptions').classList.toggle('hidden', mode !== 'resolution'); document.getElementById('filesizeOptions').classList.toggle('hidden', mode !== 'filesize'); } function openTab(evt, tabName) { const tabcontent = document.getElementsByClassName("tabcontent"); for (let i = 0; i < tabcontent.length; i++) { tabcontent[i].classList.remove("active"); } const tablinks = document.getElementsByClassName("tablinks"); for (let i = 0; i < tablinks.length; i++) { tablinks[i].classList.remove("active"); } document.getElementById(tabName).classList.add("active"); evt.currentTarget.classList.add("active"); } async function loadFFmpeg() { if (ffmpegLoaded) { showMessage('ffmpeg.wasm already loaded'); return; } showMessage('Loading ffmpeg.wasm...'); loadFFmpegBtn.disabled = true; try { await ffmpeg.load(); ffmpegLoaded = true; showMessage('ffmpeg.wasm loaded successfully'); console.log("ffmpeg loaded:", ffmpeg); compressButton.disabled = !(ffmpegLoaded && videoBlob); } catch (err) { let errorMsg = `Error loading ffmpeg: ${err.message}`; if (err.message.includes('SharedArrayBuffer') || err.message.includes('cross-origin isolated')) { errorMsg += `<br><br><strong>Server Configuration Required:</strong><br> For ffmpeg.wasm to work, your server needs these headers:<br> <code> Header set Cross-Origin-Opener-Policy "same-origin"<br> Header set Cross-Origin-Embedder-Policy "require-corp" </code><br><br> If you can't configure the server, try:<br> 1. Serving this page via localhost<br> 2. Using Chrome with <code>--disable-web-security</code> flag (for testing only)`; } showMessage(errorMsg, true); console.error("ffmpeg load error:", err); loadFFmpegBtn.disabled = false; } } function handleVideoFile(file) { if (!file) return; if (!file.type.startsWith('video/')) { showMessage("Please select a video file", true); return; } if (file.size > 100 * 1024 * 1024) { // 100MB limit showMessage("File is too large (max 100MB recommended)", true); return; } videoBlob = file; originalSize = file.size; const objectURL = URL.createObjectURL(file); fileInfo.innerHTML = ` <strong>Selected file:</strong> ${file.name}<br> <strong>Size:</strong> ${(file.size / (1024 * 1024)).toFixed(2)}MB<br> <strong>Type:</strong> ${file.type || 'unknown'} `; inputVideo.onloadedmetadata = () => { videoDuration = inputVideo.duration; originalWidth = inputVideo.videoWidth; originalHeight = inputVideo.videoHeight; // Set default width/height inputs widthInput.value = originalWidth; heightInput.value = originalHeight; aspectRatio.textContent = `${originalWidth}:${originalHeight}`; originalInfo.innerHTML = ` <strong>Dimensions:</strong> ${originalWidth}×${originalHeight}<br> <strong>Duration:</strong> ${videoDuration.toFixed(2)}s<br> <strong>Size:</strong> ${(file.size / (1024 * 1024)).toFixed(2)}MB `; showMessage(`Video loaded (${(file.size / (1024 * 1024)).toFixed(2)}MB, ${originalWidth}×${originalHeight}, ${videoDuration.toFixed(2)}s)`); compressButton.disabled = !(ffmpegLoaded && videoBlob); console.log("Video metadata loaded"); }; inputVideo.onerror = () => { showMessage("Error loading video file", true); videoBlob = null; compressButton.disabled = true; }; inputVideo.src = objectURL; } // Update height when width changes (if aspect ratio is locked) widthInput.addEventListener('input', () => { if (keepAspect.checked && originalWidth > 0) { const ratio = originalHeight / originalWidth; heightInput.value = Math.round(widthInput.value * ratio); } }); // Update width when height changes (if aspect ratio is locked) heightInput.addEventListener('input', () => { if (keepAspect.checked && originalHeight > 0) { const ratio = originalWidth / originalHeight; widthInput.value = Math.round(heightInput.value * ratio); } }); // Update quality value display qualitySlider.addEventListener('input', () => { qualityValue.textContent = qualitySlider.value; }); async function loadVideo() { const url = document.getElementById('videoUrl').value.trim(); const useProxy = document.getElementById('bypassCors').checked; if (!url) { showMessage("Please enter a video URL", true); return; } showMessage('Fetching video...'); try { const proxyUrl = useProxy ? `https://cors-anywhere.herokuapp.com/${url}` : url; console.log(`Fetching from: ${proxyUrl}`); const response = await fetch(proxyUrl, { headers: useProxy ? { 'X-Requested-With': 'XMLHttpRequest' } : {} }); if (!response.ok) { throw new Error(`HTTP ${response.status} - ${response.statusText}`); } const contentLength = response.headers.get('Content-Length'); if (contentLength && parseInt(contentLength) > 50 * 1024 * 1024) { throw new Error("Video is too large (max ~50MB recommended)"); } videoBlob = await response.blob(); originalSize = videoBlob.size; if (!videoBlob.type.startsWith('video/')) { throw new Error("The URL doesn't point to a valid video file"); } const objectURL = URL.createObjectURL(videoBlob); await new Promise((resolve, reject) => { inputVideo.onloadedmetadata = () => { videoDuration = inputVideo.duration; originalWidth = inputVideo.videoWidth; originalHeight = inputVideo.videoHeight; // Set default width/height inputs widthInput.value = originalWidth; heightInput.value = originalHeight; aspectRatio.textContent = `${originalWidth}:${originalHeight}`; originalInfo.innerHTML = ` <strong>Dimensions:</strong> ${originalWidth}×${originalHeight}<br> <strong>Duration:</strong> ${videoDuration.toFixed(2)}s<br> <strong>Size:</strong> ${(videoBlob.size / (1024 * 1024)).toFixed(2)}MB `; showMessage(`Video loaded (${(videoBlob.size / (1024 * 1024)).toFixed(2)}MB, ${originalWidth}×${originalHeight}, ${videoDuration.toFixed(2)}s)`); compressButton.disabled = !(ffmpegLoaded && videoBlob); console.log("Video metadata loaded"); resolve(); }; inputVideo.onerror = () => { reject(new Error("Video playback error")); }; inputVideo.src = objectURL; }); } catch (err) { showMessage(`Error: ${err.message}`, true); console.error("Video load error:", err); videoBlob = null; compressButton.disabled = true; } } fileUpload.addEventListener('change', (e) => { const file = e.target.files[0]; handleVideoFile(file); }); loadFFmpegBtn.onclick = loadFFmpeg; document.getElementById('loadVideo').onclick = loadVideo; document.getElementById('compressButton').onclick = async () => { if (!videoBlob) { showMessage("No video loaded", true); return; } try { showMessage('Starting compression process...'); progress.value = 0; console.log(`Writing file to MEMFS`); const fileData = videoBlob instanceof File ? await fetchFile(videoBlob) : new Uint8Array(await videoBlob.arrayBuffer()); await ffmpeg.FS('writeFile', 'input.mp4', fileData); ffmpeg.setProgress(({ ratio }) => { progress.value = Math.round(ratio * 100); showMessage(`Processing... ${progress.value}%`); }); const mode = document.querySelector('input[name="mode"]:checked').value; const audioSettings = getAudioSettings(); let command; if (mode === 'resolution') { // Resolution-based compression const width = parseInt(widthInput.value) || originalWidth; const height = parseInt(heightInput.value) || originalHeight; const crf = qualitySlider.value; command = [ '-i', 'input.mp4', '-vf', `scale=${width}:${height}`, '-c:v', 'libx264', '-crf', crf, '-preset', 'medium', ...audioSettings, 'output.mp4' ]; console.log(`Executing ffmpeg command for resolution mode: ${width}x${height}, CRF ${crf}`); } else { // Filesize-based compression const targetMB = parseFloat(targetSize.value) || 5; const allocation = bitrateAllocation.value.split('/'); const videoPercent = parseInt(allocation[0]) / 100; const audioPercent = parseInt(allocation[1]) / 100; const targetBits = Math.round(targetMB * 1024 * 1024 * 8 / videoDuration); const videoBits = Math.round(targetBits * videoPercent); const audioBits = Math.round(targetBits * audioPercent); const preset = filesizePreset.value; command = [ '-i', 'input.mp4', '-c:v', 'libx264', '-b:v', `${videoBits}`, '-maxrate', `${videoBits}`, '-bufsize', `${videoBits * 2}`, '-preset', preset, ...audioSettings.filter(opt => !opt.startsWith('-b:a:')), // Remove any existing audio bitrate '-b:a', `${audioBits}`, 'output.mp4' ]; console.log(`Executing ffmpeg command for filesize mode: ${targetMB}MB, ${preset} preset`); } await ffmpeg.run(...command); console.log(`Reading output file`); const data = ffmpeg.FS('readFile', 'output.mp4'); console.log(`Creating blob (size: ${data.length} bytes)`); const compressedBlob = new Blob([data.buffer], { type: 'video/mp4' }); outputVideo.src = URL.createObjectURL(compressedBlob); outputVideo.load(); // Show compression results const originalMB = (originalSize / (1024 * 1024)).toFixed(2); const compressedMB = (data.length / (1024 * 1024)).toFixed(2); const reduction = ((1 - (data.length / originalSize)) * 100).toFixed(1); compressedInfo.innerHTML = ` <strong>New Size:</strong> ${compressedMB}MB (${reduction}% reduction)<br> <strong>Compression Ratio:</strong> ${originalMB}MB → ${compressedMB}MB `; showMessage(`Compression complete! Reduced from ${originalMB}MB to ${compressedMB}MB (${reduction}% smaller)`); console.log("Compression process completed successfully"); // Clean up files from MEMFS try { ffmpeg.FS('unlink', 'input.mp4'); ffmpeg.FS('unlink', 'output.mp4'); } catch (cleanupErr) { console.warn("Cleanup error:", cleanupErr); } } catch (err) { showMessage(`Error during compression: ${err.message}`, true); console.error("Compression error:", err); if (err.message.includes('memory')) { showMessage("Video might be too large for available memory", true); } else if (err.message.includes('codec') || err.message.includes('format')) { showMessage("Video codec might not be supported", true); } } }; function getAudioSettings() { const codec = audioCodec.value; const bitrate = audioBitrate.value; const channels = audioChannels.value; const sampleRate = audioSampleRate.value; if (codec === 'none') { return ['-an']; // No audio } if (codec === 'copy') { return ['-c:a', 'copy']; // Copy original audio } const settings = [ '-c:a', codec, '-b:a', bitrate ]; if (channels !== 'copy') { settings.push('-ac', channels); } if (sampleRate !== 'copy') { settings.push('-ar', sampleRate); } return settings; } </script> </body> </html> Fediverse instance user info ---------------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body { font-family: Arial, sans-serif; } .user-list { margin-top: 20px; } .user-link { display: block; padding: 8px; margin: 5px 0; text-decoration: none; color: #007BFF; font-weight: bold; } .user-link:hover { text-decoration: underline; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 8px; border: 1px solid #ddd; text-align: left; } th { background-color: #f4f4f4; } details { margin-top: 20px; } details summary { font-weight: bold; cursor: pointer; } </style> </head> <body> <form id="endpoint-form"> <label for="domain">Enter Domain:</label> <input type="text" id="domain" name="domain" placeholder="e.g., mastodon.social" required> <button type="submit">Fetch Users</button> </form> <div id="user-count"></div> <div class="user-list" id="user-list"> <!-- Users will be listed here --> </div> <!-- Instance Data in a details section --> <details id="instance-details"> <summary>Click to View Instance Information</summary> <table id="instance-table"> <!-- Instance data will be displayed here --> </table> </details> <script> // Function to get query string value for 'instance' function getQueryStringParameter(name) { const urlParams = new URLSearchParams(window.location.search); return urlParams.get(name); } // Automatically trigger fetch if 'instance' query parameter is provided const instanceFromQuery = getQueryStringParameter('instance'); if (instanceFromQuery) { document.getElementById('domain').value = instanceFromQuery; fetchData(instanceFromQuery); // Auto trigger fetch for the instance } document.getElementById('endpoint-form').addEventListener('submit', async (e) => { e.preventDefault(); const domain = document.getElementById('domain').value.trim(); if (!domain) { alert('Please enter a valid domain'); return; } fetchData(domain); }); async function fetchData(domain) { const endpointDirectory = `https://${domain}/api/v1/directory?local=true`; const endpointInstance = `https://${domain}/api/v1/instance`; const userListContainer = document.getElementById('user-list'); const userCountContainer = document.getElementById('user-count'); const instanceTable = document.getElementById('instance-table'); const instanceDetails = document.getElementById('instance-details'); userListContainer.innerHTML = 'Loading...'; userCountContainer.innerHTML = ''; // Clear any previous count try { // Fetch the instance data to get the total user count and other details const instanceResponse = await fetch(endpointInstance); if (!instanceResponse.ok) { throw new Error('Failed to fetch instance data.'); } const instanceData = await instanceResponse.json(); const totalUsers = instanceData.stats ? instanceData.stats.user_count : 'Unknown'; // Display total user count userCountContainer.innerHTML = `Total Users: ${totalUsers}`; // Fetch the users from the directory endpoint const directoryResponse = await fetch(endpointDirectory); if (!directoryResponse.ok) { throw new Error('Failed to fetch users from the directory.'); } const data = await directoryResponse.json(); if (data && Array.isArray(data)) { // Sort users by `created_at` (registration date) const sortedUsers = data.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); userListContainer.innerHTML = ''; // Clear loading message sortedUsers.forEach(user => { // Format user data const userName = user.display_name || 'Unknown User'; const userUsername = user.username || 'Unknown Username'; const registrationDate = new Date(user.created_at).toLocaleDateString() || 'Invalid Date'; const userLink = document.createElement('a'); userLink.href = `https://${domain}/users/${userUsername}`; // Link to the user's profile userLink.classList.add('user-link'); userLink.textContent = `${userName} (@${userUsername}) - Registered: ${registrationDate}`; // Open link in a new tab userLink.target = "_blank"; userListContainer.appendChild(userLink); }); } else { userListContainer.innerHTML = 'No users found or invalid response format.'; } // Display instance data in a table const instanceDetailsHTML = ` <tr><th>Instance Title</th><td>${instanceData.title || 'N/A'}</td></tr> <tr><th>Description</th><td>${instanceData.description || 'N/A'}</td></tr> <tr><th>Email</th><td><a href="mailto:${instanceData.email || 'N/A'}">${instanceData.email || 'N/A'}</a></td></tr> <tr><th>Version</th><td>${instanceData.version || 'N/A'}</td></tr> <tr><th>Registrations</th><td>${instanceData.registrations ? 'Enabled' : 'Disabled'}</td></tr> <tr><th>Approval Required</th><td>${instanceData.approval_required ? 'Yes' : 'No'}</td></tr> <tr><th>Invites Enabled</th><td>${instanceData.invites_enabled ? 'Yes' : 'No'}</td></tr> <tr><th>Total Users</th><td>${instanceData.stats.user_count || 'N/A'}</td></tr> <tr><th>Total Statuses</th><td>${instanceData.stats.status_count || 'N/A'}</td></tr> <tr><th>Total Domains</th><td>${instanceData.stats.domain_count || 'N/A'}</td></tr> `; instanceTable.innerHTML = instanceDetailsHTML; // Enable the details section instanceDetails.open = false; } catch (error) { console.error(error); userListContainer.innerHTML = 'Error fetching data. Please try again.'; } } </script> </body> </html> PmxFromZip Loader (via Service worker, with vmd anim) -------------------------------- zipfsmmd-sw.js -- // zipfsmmd-sw.js importScripts('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'); let ZIP_PATH = null; function swLog(msg) { console.log('[PMX-ZIP SW]', msg); self.clients.matchAll().then(clients => { clients.forEach(client => { client.postMessage({ type: 'SW_LOG', text: msg }); }); }); } self.addEventListener('install', event => { swLog('Install event fired'); self.skipWaiting(); }); self.addEventListener('activate', event => { swLog('Activate event fired'); event.waitUntil(self.clients.claim()); }); self.addEventListener('message', event => { if (event.data && event.data.type === 'SET_ZIP_PATH') { ZIP_PATH = event.data.zipPath; swLog(`ZIP path set to: ${ZIP_PATH}`); zipCache.clear(); } }); const zipCache = new Map(); self.addEventListener('fetch', event => { if (!ZIP_PATH) return; // ZIP path not set, fallback to normal fetch const url = new URL(event.request.url); if (url.origin !== self.location.origin) return; const relPath = url.pathname; if (zipCache.has(relPath)) { swLog(`Serving from cache: ${relPath}`); event.respondWith(zipCache.get(relPath).clone()); return; } event.respondWith( (async () => { try { if (zipCache.size === 0) { swLog(`Fetching ZIP from ${ZIP_PATH}`); const res = await fetch(ZIP_PATH); if (!res.ok) throw new Error(`Failed to fetch ZIP: ${res.status}`); const arrayBuffer = await res.arrayBuffer(); const zip = await JSZip.loadAsync(arrayBuffer); for (const filename of Object.keys(zip.files)) { const file = zip.files[filename]; if (file.dir) continue; const fileData = await file.async('arraybuffer'); zipCache.set('/' + filename, new Response(fileData)); swLog(`Extracted: /${filename} (${fileData.byteLength} bytes)`); } swLog('Unzip complete'); } if (zipCache.has(relPath)) { return zipCache.get(relPath).clone(); } return fetch(event.request); } catch (e) { swLog(`ZIP fetch/unzip error: ${e}`); return fetch(event.request); } })() ); }); pmxziploader.html -- <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>PMX Loader from ZIP</title> <style> body { margin: 0; overflow: hidden; background: #000; font-family: monospace; } #loading { position: absolute; top: 10px; left: 10px; color: #0f0; background: #000; padding: 4px 10px; font-size: 14px; z-index: 10; } #log { position: absolute; bottom: 0; left: 0; width: 100%; height: 150px; overflow-y: auto; background: rgba(0,0,0,.8); color: #0f0; font-size: 12px; padding: 5px; box-sizing: border-box; white-space: pre-wrap; } #controls { position: absolute; top: 10px; right: 10px; z-index: 10; display: flex; flex-direction: column; } .control-btn { font-size: 20px; padding: 10px; margin: 5px; background: #333; color: #fff; border: none; border-radius: 6px; cursor: pointer; user-select: none; } .error { color: #f00; } .warning { color: #ff0; } .info { color: #0f0; } #wireframeBtn { font-size: 16px; } </style> </head> <body> <div id="loading">Initializing PMX loader...</div> <div id="log"></div> <div id="controls"> <button class="control-btn" id="zoomIn">+</button> <button class="control-btn" id="zoomOut">−</button> <button class="control-btn" id="wireframeBtn">Wireframe OFF</button> </div> <script src="./libs/three.js"></script> <script src="./libs/mmdparser-obsolete.min.js"></script> <script src="./libs/ammo.min.js"></script> <script src="./libs/TGALoader.js"></script> <script src="./libs/MMDLoader.js"></script> <script src="./libs/MMDAnimationHelper.js"></script> <script src="./libs/CCDIKSolver.js"></script> <script src="./libs/MMDPhysics.js"></script> <script> // Override console methods to capture all output const originalConsole = { log: console.log, warn: console.warn, error: console.error }; function logToDiv(type, ...args) { const msg = args.join(' '); const logEl = document.getElementById('log'); const className = type === 'error' ? 'error' : type === 'warn' ? 'warning' : 'info'; logEl.innerHTML += `<span class="${className}">${msg}</span>\n`; logEl.scrollTop = logEl.scrollHeight; } function log(...args) { const msg = args.join(' '); originalConsole.log(msg); logToDiv('info', msg); } function warn(...args) { const msg = args.join(' '); originalConsole.warn(msg); logToDiv('warn', msg); } function error(...args) { const msg = args.join(' '); originalConsole.error(msg); logToDiv('error', msg); } // Override console methods console.log = log; console.warn = warn; console.error = error; // Capture unhandled errors window.addEventListener('error', function(e) { error(`Unhandled error: ${e.message} (${e.filename}:${e.lineno}:${e.colno})`); if (e.error && e.error.stack) { error(e.error.stack); } }); // Capture unhandled promise rejections window.addEventListener('unhandledrejection', function(e) { error('Unhandled promise rejection:', e.reason); if (e.reason && e.reason.stack) { error(e.reason.stack); } }); // Track texture loading errors let missingTextures = 0; // Patch THREE.TextureLoader to catch texture loading errors const originalLoad = THREE.TextureLoader.prototype.load; THREE.TextureLoader.prototype.load = function(url, onLoad, onProgress, onError) { const patchedOnError = (err) => { missingTextures++; error(`Failed to load texture: ${url}`); if (onError) onError(err); }; return originalLoad.call(this, url, onLoad, onProgress, patchedOnError); }; // Patch ImageLoader to catch image loading errors const originalImageLoad = THREE.ImageLoader.prototype.load; THREE.ImageLoader.prototype.load = function(url, onLoad, onProgress, onError) { const patchedOnError = (err) => { missingTextures++; error(`Failed to load image: ${url}`); if (onError) onError(err); }; return originalImageLoad.call(this, url, onLoad, onProgress, patchedOnError); }; const currentDir = new URL('.', window.location.href).href; const swPath = new URL('zipfsmmd-sw.js', currentDir).href; const FALLBACK_ZIP = new URL('models/AoiZaizen.zip', currentDir).href; function getZipUrl(){ const params = new URLSearchParams(window.location.search); const zip = params.get('pmx'); if(zip) return new URL(zip, window.location.href).href; log('No ?pmx= query parameter found; falling back to default ZIP'); return FALLBACK_ZIP; } function getPmxFileName(){ const params = new URLSearchParams(window.location.search); let modelFile = params.get('model'); if(modelFile){ if(!modelFile.startsWith('/')) modelFile = '/' + modelFile; return modelFile; } const zipUrl = getZipUrl(); const zipBase = zipUrl.split('/').pop().replace(/\.zip$/i, ''); return '/' + zipBase + '.pmx'; } const zipPath = getZipUrl(); const pmxFileName = getPmxFileName(); const vmdName = 'bts-bestofme'; if('serviceWorker' in navigator){ navigator.serviceWorker.register(swPath).then(reg=>{ log(`Service Worker registered at: ${swPath}`); if(!navigator.serviceWorker.controller) setTimeout(()=>location.reload(),500); else{ navigator.serviceWorker.controller.postMessage({type:'SET_ZIP_PATH',zipPath}); initLoader(); } }).catch(err=>{ error('SW registration failed:', err); initLoader(); }); navigator.serviceWorker.addEventListener('message',event=>{ if(event.data && event.data.type==='SW_LOG') log('[SW]', event.data.text); }); }else{ warn('Service Workers not supported'); initLoader(); } async function initLoader(){ log('Initializing PMX loader...'); log(`ZIP path: ${zipPath}`); log(`PMX model path inside ZIP: ${pmxFileName}`); log(`VMD name: ${vmdName}`); let scene, renderer, camera, mesh, helper; let ready = false; const clock = new THREE.Clock(); let theta = Math.PI / 4, phi = Math.PI / 4, radius = 50; let isMouseDown = false, previousMousePosition = {x: 0, y: 0}; function initScene(){ scene = new THREE.Scene(); scene.add(new THREE.AmbientLight(0xeeeeee)); renderer = new THREE.WebGLRenderer({alpha:true,antialias:true}); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); camera = new THREE.PerspectiveCamera(40, window.innerWidth/window.innerHeight, 1, 1000); updateCameraPosition(); document.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); window.addEventListener('resize', onWindowResize); // Touch controls document.addEventListener('touchstart', onTouchStart); document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); document.getElementById('zoomIn').addEventListener('click', ()=>zoomCamera(-5)); document.getElementById('zoomOut').addEventListener('click', ()=>zoomCamera(5)); // Wireframe toggle button document.getElementById('wireframeBtn').addEventListener('click', toggleWireframe); } function toggleWireframe() { if (!mesh) return; let isWireframe = false; mesh.traverse(function(child) { if (child.isMesh) { if (Array.isArray(child.material)) { child.material.forEach(mat => { mat.wireframe = !mat.wireframe; isWireframe = mat.wireframe || isWireframe; // If textures are missing, set wireframe color to light green if (mat.wireframe && missingTextures > 0) { mat.color.set(0x90EE90); // Light green mat.emissive.set(0x90EE90); mat.needsUpdate = true; } else if (!mat.wireframe) { // Reset to default colors when wireframe is off mat.color.set(0xFFFFFF); mat.emissive.set(0x000000); mat.needsUpdate = true; } }); } else { child.material.wireframe = !child.material.wireframe; isWireframe = child.material.wireframe || isWireframe; // If textures are missing, set wireframe color to light green if (child.material.wireframe && missingTextures > 0) { child.material.color.set(0x90EE90); // Light green child.material.emissive.set(0x90EE90); child.material.needsUpdate = true; } else if (!child.material.wireframe) { // Reset to default colors when wireframe is off child.material.color.set(0xFFFFFF); child.material.emissive.set(0x000000); child.material.needsUpdate = true; } } } }); document.getElementById('wireframeBtn').textContent = isWireframe ? 'Wireframe ON' : 'Wireframe OFF'; log(`Wireframe mode ${isWireframe ? 'enabled' : 'disabled'}`); if (isWireframe && missingTextures > 0) { log(`Using light green wireframe (${missingTextures} missing textures)`); } } function onMouseDown(e){e.preventDefault(); isMouseDown=true; previousMousePosition={x:e.clientX,y:e.clientY};} function onMouseMove(e){ if(!isMouseDown) return; e.preventDefault(); const dx = e.clientX - previousMousePosition.x; const dy = e.clientY - previousMousePosition.y; theta -= dx * 0.005; phi -= dy * 0.005; phi = Math.min(Math.max(phi,0.01), Math.PI-0.01); updateCameraPosition(); previousMousePosition = {x:e.clientX,y:e.clientY}; } function onMouseUp(e){e.preventDefault(); isMouseDown=false;} // Touch controls let touchStartX = 0, touchStartY = 0; function onTouchStart(e) { e.preventDefault(); touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } function onTouchMove(e) { e.preventDefault(); const dx = e.touches[0].clientX - touchStartX; const dy = e.touches[0].clientY - touchStartY; theta -= dx * 0.005; phi -= dy * 0.005; phi = Math.min(Math.max(phi, 0.01), Math.PI - 0.01); updateCameraPosition(); touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } function onTouchEnd(e) { } function zoomCamera(delta){ radius += delta; radius = Math.min(Math.max(radius,10),200); updateCameraPosition(); } function updateCameraPosition(){ camera.position.x = radius * Math.sin(phi) * Math.cos(theta); camera.position.y = radius * Math.cos(phi); camera.position.z = radius * Math.sin(phi) * Math.sin(theta); camera.lookAt(scene.position); } function onWindowResize(){ camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } async function loadModel(){ document.getElementById('loading').textContent = 'Loading model from ZIP...'; return new Promise((resolve, reject) => { const loader = new THREE.MMDLoader(); // Patch MMDLoader's texture loading const originalLoadTexture = loader._loadTexture; loader._loadTexture = function(path, modelPath, onLoad, onProgress, onError) { const patchedOnError = (err) => { missingTextures++; error(`Failed to load MMD texture: ${path}`); if (onError) onError(err); }; return originalLoadTexture.call(this, path, modelPath, onLoad, onProgress, patchedOnError); }; loader.load(pmxFileName, object => { mesh = object; mesh.position.y = -10; scene.add(mesh); document.getElementById('loading').textContent = 'Loading animation...'; log('PMX model loaded successfully from ZIP'); if (missingTextures > 0) { warn(`${missingTextures} textures failed to load`); } resolve(); }, xhr => { if(xhr.lengthComputable){ const percent = (xhr.loaded / xhr.total) * 100; document.getElementById('loading').textContent = `Loading model... ${percent.toFixed(1)}%`; } }, err => { error('Model load error:', err); document.getElementById('loading').textContent = 'Model load failed!'; reject(err); }); }); } async function loadAnimation(){ return new Promise((resolve, reject) => { const loader = new THREE.MMDLoader(); const vmdPath = new URL(`vmd/${vmdName}.vmd`, currentDir).href; log(`Loading VMD from: ${vmdPath}`); loader.loadAnimation(vmdPath, mesh, vmdClip => { vmdClip.name = vmdName; setupAnimation(vmdClip); document.getElementById('loading').style.display = 'none'; log('Animation loaded successfully'); resolve(); }, xhr => { if(xhr.lengthComputable){ const percent = (xhr.loaded / xhr.total) * 100; document.getElementById('loading').textContent = `Loading animation... ${percent.toFixed(1)}%`; } }, err => { error('Animation load error:', err); document.getElementById('loading').textContent = 'Animation load failed!'; reject(err); }); }); } function setupAnimation(vmdClip){ try { ready = false; helper = new THREE.MMDAnimationHelper({afterglow: 2, resetPhysicsOnLoop: true}); helper.add(mesh, { animation: vmdClip, physics: true }); const mixer = helper.objects.get(mesh).mixer; mixer.addEventListener('finished', () => { log('Animation finished. Restarting...'); setupAnimation(vmdClip); }); ready = true; } catch (e) { error('Error setting up animation:', e); throw e; } } function animate(){ try { requestAnimationFrame(animate); if(ready && helper) helper.update(clock.getDelta()); renderer.render(scene, camera); } catch (e) { error('Animation loop error:', e); } } initScene(); try { await loadModel(); await loadAnimation(); animate(); log('PMX viewer ready'); } catch(e) { error('Initialization failed:', e); } } </script> </body> </html> <!--- ammo.min.js 2b9e0d61ee837ab1f8d2c189d3182b65 CCDIKSolver.js 14a67e16f1620e86a1bf47add2cf8dc2 MMDAnimationHelper.js ada4ff3d55c28f87b9ba301bb82fac7e MMDLoader.js 12d36dd5a55d70736214c0fc316af320 MMDLoader.js.orig f82d61afe34c7de18b0adc3d1f59348b mmd-loader.min.js c3041dcf588c2cc3f8a9866497ee787a mmdparser.min.js 543372130dab2337cc8158b904068a28 mmdparser-obsolete.min.js 543372130dab2337cc8158b904068a28 MMDPhysics.js 3d6ea9133522d293b69d8db6eaff00dd OrbitControls.js e1aab2c938f25bbac4b6d327e551fac0 OutlineEffect.js 14a675124f646040d5a34cea3f3c854c Stats.min.js b134a94301558097e6880e870dd0c166 TGALoader.js a98fce8285c995df26eb9a00f915ec52 three.js.noboneerr 828532a6c967fa58a79ef10ad49d9f24 three.min.js eb8549863a97355411c3259a3f93b8e1 three.module.js 9274bccc27ba09a42f61eb971687f78d three.module.min.js 8477f142c1d8e670968cb678788ce041 Vmd.js 5b6f1505c7b6b0e32e5bb26ec33c54c4 VmdFileParser.js a4854fd3bc0aa2b86e1d8adc9425149e ---> Ffmpeg trimmer ------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>MP4 Trimmer with ffmpeg.wasm</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: auto; padding: 20px; } input[type=range] { width: 100%; } label { display: block; margin-top: 10px; } video { max-width: 100%; margin-top: 10px; } pre#debugLog { background: #111; color: #0f0; padding: 10px; overflow: auto; max-height: 200px; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; } #progress, #ffmpegProgress { width: 100%; height: 16px; } button { margin-top: 10px; } #message { margin-top: 10px; font-weight: bold; } .error { color: red; } .success { color: green; } .tab { overflow: hidden; border: 1px solid #ccc; background-color: #f1f1f1; border-radius: 4px 4px 0 0; } .tab button { background-color: inherit; float: left; border: none; outline: none; cursor: pointer; padding: 10px 16px; transition: 0.3s; margin-top: 0; } .tab button:hover { background-color: #ddd; } .tab button.active { background-color: #ccc; } .tabcontent { display: none; padding: 15px; border: 1px solid #ccc; border-top: none; border-radius: 0 0 4px 4px; } .tabcontent.active { display: block; } #fileInfo { margin: 10px 0; font-style: italic; } </style> </head> <body> <h2>Trim MP4 Video</h2> <div class="tab"> <button class="tablinks active" onclick="openTab(event, 'urlTab')">From URL</button> <button class="tablinks" onclick="openTab(event, 'uploadTab')">Upload File</button> </div> <div id="urlTab" class="tabcontent active"> <input type="text" id="videoUrl" placeholder="Enter MP4 URL" size="60" /> <label><input type="checkbox" id="bypassCors" /> Bypass CORS via proxy</label> <button id="loadVideo">Load Video</button> </div> <div id="uploadTab" class="tabcontent"> <input type="file" id="fileUpload" accept="video/mp4" /> <div id="fileInfo"></div> </div> <button id="loadFFmpegBtn">Load ffmpeg.wasm</button> <div id="message"></div> <video id="inputVideo" controls></video> <label>Start Time: <span id="startLabel">0</span> sec</label> <input type="range" id="startSlider" min="0" max="100" value="0" step="0.1" /> <label>End Time: <span id="endLabel">0</span> sec</label> <input type="range" id="endSlider" min="0" max="100" value="100" step="0.1" /> <button id="trimButton" disabled>Trim Video</button> <progress id="progress" value="0" max="100"></progress> <h3>Trimmed Output</h3> <video id="outputVideo" controls></video> <h3>Debug Log</h3> <pre id="debugLog"></pre> <!-- Updated ffmpeg versions --> <script src="https://unpkg.com/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js"></script> <script src="https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js"></script> <script> const logBox = document.getElementById('debugLog'); const message = document.getElementById('message'); const inputVideo = document.getElementById('inputVideo'); const outputVideo = document.getElementById('outputVideo'); const startSlider = document.getElementById('startSlider'); const endSlider = document.getElementById('endSlider'); const startLabel = document.getElementById('startLabel'); const endLabel = document.getElementById('endLabel'); const progress = document.getElementById('progress'); const loadFFmpegBtn = document.getElementById('loadFFmpegBtn'); const trimButton = document.getElementById('trimButton'); const fileUpload = document.getElementById('fileUpload'); const fileInfo = document.getElementById('fileInfo'); // Enhanced console logging ['log', 'error', 'warn', 'info'].forEach(method => { const original = console[method]; console[method] = function(...args) { const msg = args.map(a => { try { return (typeof a === 'object') ? JSON.stringify(a, null, 2) : String(a); } catch { return String(a); } }).join(' '); const time = new Date().toLocaleTimeString(); logBox.textContent += `[${time}] ${method.toUpperCase()}: ${msg}\n`; logBox.scrollTop = logBox.scrollHeight; original.apply(console, args); }; }); const { createFFmpeg, fetchFile } = FFmpeg; const ffmpeg = createFFmpeg({ log: true, corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js' }); let videoBlob = null; let videoDuration = 0; let ffmpegLoaded = false; function showMessage(text, isError = false) { message.textContent = text; message.className = isError ? 'error' : 'success'; } function openTab(evt, tabName) { const tabcontent = document.getElementsByClassName("tabcontent"); for (let i = 0; i < tabcontent.length; i++) { tabcontent[i].classList.remove("active"); } const tablinks = document.getElementsByClassName("tablinks"); for (let i = 0; i < tablinks.length; i++) { tablinks[i].classList.remove("active"); } document.getElementById(tabName).classList.add("active"); evt.currentTarget.classList.add("active"); } async function loadFFmpeg() { if (ffmpegLoaded) { showMessage('ffmpeg.wasm already loaded'); return; } showMessage('Loading ffmpeg.wasm...'); loadFFmpegBtn.disabled = true; try { await ffmpeg.load(); ffmpegLoaded = true; showMessage('ffmpeg.wasm loaded successfully'); console.log("ffmpeg loaded:", ffmpeg); trimButton.disabled = !(ffmpegLoaded && videoBlob); } catch (err) { let errorMsg = `Error loading ffmpeg: ${err.message}`; // Add specific hint for COOP/COEP headers if (err.message.includes('SharedArrayBuffer') || err.message.includes('cross-origin isolated')) { errorMsg += `<br><br><strong>Server Configuration Required:</strong><br> For ffmpeg.wasm to work, your server needs these headers:<br> <code> Header set Cross-Origin-Opener-Policy "same-origin"<br> Header set Cross-Origin-Embedder-Policy "require-corp" </code><br><br> If you can't configure the server, try:<br> 1. Serving this page via localhost<br> 2. Using Chrome with <code>--disable-web-security</code> flag (for testing only)`; } showMessage(errorMsg, true); console.error("ffmpeg load error:", err); loadFFmpegBtn.disabled = false; } } function handleVideoFile(file) { if (!file) return; if (!file.type.startsWith('video/') && !file.name.endsWith('.mp4')) { showMessage("Please select an MP4 video file", true); return; } if (file.size > 100 * 1024 * 1024) { // 100MB limit showMessage("File is too large (max 100MB recommended)", true); return; } videoBlob = file; const objectURL = URL.createObjectURL(file); fileInfo.innerHTML = ` <strong>Selected file:</strong> ${file.name}<br> <strong>Size:</strong> ${(file.size / (1024 * 1024)).toFixed(2)}MB<br> <strong>Type:</strong> ${file.type || 'unknown'} `; inputVideo.onloadedmetadata = () => { videoDuration = inputVideo.duration; startSlider.max = endSlider.max = videoDuration.toFixed(2); startSlider.value = 0; endSlider.value = videoDuration.toFixed(2); startLabel.textContent = "0"; endLabel.textContent = videoDuration.toFixed(2); showMessage(`Video loaded (${(file.size / (1024 * 1024)).toFixed(2)}MB, ${videoDuration.toFixed(2)}s)`); trimButton.disabled = !(ffmpegLoaded && videoBlob); console.log("Video metadata loaded"); }; inputVideo.onerror = () => { showMessage("Error loading video file", true); videoBlob = null; trimButton.disabled = true; }; inputVideo.src = objectURL; } async function loadVideo() { const url = document.getElementById('videoUrl').value.trim(); const useProxy = document.getElementById('bypassCors').checked; if (!url) { showMessage("Please enter a video URL", true); return; } showMessage('Fetching video...'); try { const proxyUrl = useProxy ? `https://cors-anywhere.herokuapp.com/${url}` : url; console.log(`Fetching from: ${proxyUrl}`); const response = await fetch(proxyUrl, { headers: useProxy ? { 'X-Requested-With': 'XMLHttpRequest' } : {} }); if (!response.ok) { throw new Error(`HTTP ${response.status} - ${response.statusText}`); } const contentLength = response.headers.get('Content-Length'); if (contentLength && parseInt(contentLength) > 50 * 1024 * 1024) { throw new Error("Video is too large (max ~50MB recommended)"); } videoBlob = await response.blob(); // Validate it's actually a video file if (!videoBlob.type.startsWith('video/')) { throw new Error("The URL doesn't point to a valid video file"); } const objectURL = URL.createObjectURL(videoBlob); await new Promise((resolve, reject) => { inputVideo.onloadedmetadata = () => { videoDuration = inputVideo.duration; startSlider.max = endSlider.max = videoDuration.toFixed(2); startSlider.value = 0; endSlider.value = videoDuration.toFixed(2); startLabel.textContent = "0"; endLabel.textContent = videoDuration.toFixed(2); showMessage(`Video loaded (${(videoBlob.size / (1024 * 1024)).toFixed(2)}MB, ${videoDuration.toFixed(2)}s)`); trimButton.disabled = !(ffmpegLoaded && videoBlob); console.log("Video metadata loaded"); resolve(); }; inputVideo.onerror = () => { reject(new Error("Video playback error")); }; inputVideo.src = objectURL; }); } catch (err) { showMessage(`Error: ${err.message}`, true); console.error("Video load error:", err); videoBlob = null; trimButton.disabled = true; } } startSlider.oninput = () => { const startValue = parseFloat(startSlider.value); const endValue = parseFloat(endSlider.value); if (startValue > endValue) { startSlider.value = endValue; } startLabel.textContent = parseFloat(startSlider.value).toFixed(2); inputVideo.currentTime = parseFloat(startSlider.value); }; endSlider.oninput = () => { const startValue = parseFloat(startSlider.value); const endValue = parseFloat(endSlider.value); if (endValue < startValue) { endSlider.value = startValue; } endLabel.textContent = parseFloat(endSlider.value).toFixed(2); inputVideo.currentTime = parseFloat(endSlider.value); }; fileUpload.addEventListener('change', (e) => { const file = e.target.files[0]; handleVideoFile(file); }); loadFFmpegBtn.onclick = loadFFmpeg; document.getElementById('loadVideo').onclick = loadVideo; document.getElementById('trimButton').onclick = async () => { if (!videoBlob) { showMessage("No video loaded", true); return; } const start = parseFloat(startSlider.value); const end = parseFloat(endSlider.value); const duration = end - start; if (duration <= 0) { showMessage("End time must be after start time", true); return; } try { showMessage('Starting trim process...'); progress.value = 0; console.log(`Writing file to MEMFS`); const fileData = videoBlob instanceof File ? await fetchFile(videoBlob) : new Uint8Array(await videoBlob.arrayBuffer()); await ffmpeg.FS('writeFile', 'input.mp4', fileData); ffmpeg.setProgress(({ ratio }) => { progress.value = Math.round(ratio * 100); showMessage(`Processing... ${progress.value}%`); }); console.log(`Executing ffmpeg command`); await ffmpeg.run( '-i', 'input.mp4', '-ss', `${start}`, '-t', `${duration}`, '-c:v', 'copy', // Use stream copy for faster processing '-c:a', 'copy', '-avoid_negative_ts', '1', 'output.mp4' ); console.log(`Reading output file`); const data = ffmpeg.FS('readFile', 'output.mp4'); console.log(`Creating blob (size: ${data.length} bytes)`); const trimmedBlob = new Blob([data.buffer], { type: 'video/mp4' }); outputVideo.src = URL.createObjectURL(trimmedBlob); outputVideo.load(); showMessage(`Trim complete! Output: ${(data.length / (1024 * 1024)).toFixed(2)}MB`); console.log("Trim process completed successfully"); // Clean up files from MEMFS try { ffmpeg.FS('unlink', 'input.mp4'); ffmpeg.FS('unlink', 'output.mp4'); } catch (cleanupErr) { console.warn("Cleanup error:", cleanupErr); } } catch (err) { showMessage(`Error during trimming: ${err.message}`, true); console.error("Trim error:", err); // Additional error details for common issues if (err.message.includes('memory')) { showMessage("Video might be too large for available memory", true); } else if (err.message.includes('codec') || err.message.includes('format')) { showMessage("Video codec might not be supported", true); } } }; </script> </body> </html> Mastodon Reply handler (+ Akkoma) ---------------------- <!DOCTYPE html> <base href="https://" target="_blank" rel="noopener noreferrer"> <html lang="en"> <head> <meta charset="UTF-8"> <title>Akkoma Post Viewer</title> <style> body { font-family: sans-serif; //background-color: #f5f5f5; //padding: 2rem; } .toot { background: white; padding: 1rem; border-radius: 0px; max-width: 600px; box-shadow: 0 0 20px rgba(0,0,0,0.1); display: flex; gap: 1rem; margin-bottom: 1rem; flex-direction: column; } .avatar { width: 48px; height: 48px; border-radius: 50%; flex-shrink: 0; } .content { flex: 1; } .user { font-weight: bold; } .handle { color: gray; font-size: 0.9em; margin-left: 0.3em; } .post-content { margin: 0.5em 0; } .counts { color: gray; font-size: 0.9em; display: flex; gap: 1em; } .view-post { margin-top: 0em; font-size: 0.9em; color: #0077cc; } .view-post a { text-decoration: none; color: inherit; } .reply { margin-left: 2rem; /* Indent replies */ } .parent-stats { font-size: 1rem; color: gray; margin-bottom: 1rem; } </style> </head> <body> <div id="results"></div> <script> async function fetchEmojis(instance) { const url = `https://${instance}/api/v1/custom_emojis`; try { const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch emojis"); return await response.json(); } catch (error) { console.error("Error fetching emojis: ", error); return []; } } function replaceEmojis(content, emojis) { emojis.forEach(emoji => { const regex = new RegExp(`:${emoji.shortcode}:`, 'g'); const imgTag = `<img src="${emoji.url}" alt=":${emoji.shortcode}:" class="emoji" style="width:20px; height:20px;">`; content = content.replace(regex, imgTag); }); return content; } async function fetchPostStats(postUrl) { const { instance, statusId } = extractInstanceAndStatus(postUrl); try { const response = await fetch(`https://${instance}/api/v1/statuses/${statusId}`); if (!response.ok) throw new Error("Failed to fetch post"); const data = await response.json(); const acct = data.account; const emojis = await fetchEmojis(instance); let content = replaceEmojis(data.content, emojis); const fullPostUrl = data.url; const postHtml = ` <div class="toot"> <div class="header"> <div class="content"> <div class="parent-stats"> </div> <div class="counts"> ❤️ ${data.favourites_count} 🔁 ${data.reblogs_count} 💬 ${data.replies_count} </div> <div class="view-post"> <!--<a href="${fullPostUrl}" target="_blank">View This Post</a>--> </div> </div> </div> <div class="replies" id="replies-${statusId}"></div> </div> `; const repliesContainer = document.createElement('div'); repliesContainer.innerHTML = postHtml; document.getElementById('results').appendChild(repliesContainer); if (data.replies_count > 0) { await fetchReplies(statusId, instance); } } catch (error) { document.getElementById('results').innerHTML = `<p>Error: ${error.message}</p>`; } } async function fetchReplies(statusId, instance) { const url = `https://${instance}/api/v1/statuses/${statusId}/context`; try { const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch replies"); const data = await response.json(); const replies = data.descendants; const repliesContainer = document.getElementById(`replies-${statusId}`); for (const reply of replies) { const replyHtml = await renderPost(reply, instance); repliesContainer.innerHTML += replyHtml; if (reply.replies_count > 0) { await fetchReplies(reply.id, instance); } } } catch (error) { console.error("Error fetching replies: ", error); } } async function renderPost(post, instance) { const acct = post.account; const postUrl = post.url; // Use correct canonical post URL const emojis = await fetchEmojis(instance); let content = replaceEmojis(post.content, emojis); return ` <div class="toot reply"> <div class="header"> <img src="${acct.avatar_static}" alt="avatar" class="avatar"> <div class="content"> <div class="user"> ${acct.display_name || acct.username} <span class="handle">@${acct.acct}</span> </div> <div class="post-content">${content}</div> <div class="counts"> ❤️ ${post.favourites_count} 🔁 ${post.reblogs_count} 💬 ${post.replies_count} </div> <div class="view-post"> <a href="${postUrl}" target="_blank">View This Reply</a> </div> </div> </div> </div> `; } function extractInstanceAndStatus(postUrl) { const urlParts = postUrl.split('/'); const instance = urlParts[2]; const statusId = urlParts[urlParts.length - 1]; return { instance, statusId }; } const urlParams = new URLSearchParams(window.location.search); const postUrl = urlParams.get('url'); if (postUrl) { fetchPostStats(postUrl); } else { fetchPostStats('https://mas.to/@alcea/114893352699203026'); } </script> Fediverse Remote Account Suspension Checker ------------------------------ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Mastodon Suspension Check</title> <style> body { font-family: Arial, sans-serif; padding: 1rem; max-width: 700px; margin: auto; } label { display: block; margin-top: 1rem; font-weight: bold; } input[type="text"] { width: 100%; padding: 0.4rem; font-size: 1rem; } button { margin-top: 1rem; padding: 0.5rem 1rem; font-size: 1rem; cursor: pointer; } ul { list-style: none; padding-left: 0; margin-top: 1rem; } li { margin-bottom: 0.5rem; } .loading { color: gray; } .suspended { color: red; font-weight: bold; } .active { color: green; font-weight: bold; } .error { color: orange; font-weight: bold; } .lookup-domain { margin-left: 0.5rem; font-size: 0.9em; color: #555; font-family: monospace; text-decoration: none; } .lookup-domain:hover { text-decoration: underline; } .checkbox-label { font-weight: normal; margin-top: 0.5rem; display: flex; align-items: center; gap: 0.4rem; } </style> </head> <body> <h1>Am I suspended ? </h1> <ul id="results"> <!-- results go here --> </ul> <form id="checkForm"> <label for="acctInput">User handle to check (e.g. alceawis@alceawis.com):</label> <input type="text" id="acctInput" value="alceawis@alceawis.com" required /> <label for="followersUrlInput">Followers JSON endpoint URL:</label> <input type="text" id="followersUrlInput" value="https://alceawis.com/alceawis/followers" required /> <label class="checkbox-label"> <input type="checkbox" id="formatToggle" /> Use <code>normal Mastodon followers</code> JSON format </label> <button type="submit">Check Suspension Status</button> </form> <script> async function runCheck() { const acctToCheck = document.getElementById('acctInput').value.trim(); const userFollowersUrl = document.getElementById('followersUrlInput').value.trim(); const useNormalFormat = document.getElementById('formatToggle').checked; const resultsEl = document.getElementById('results'); resultsEl.innerHTML = '<li class="loading">Loading followers...</li>'; if (!acctToCheck || !userFollowersUrl) { resultsEl.innerHTML = '<li class="error">Please fill in both fields.</li>'; return; } resultsEl.innerHTML = ''; const extractDomain = url => { try { return new URL(url).hostname; } catch { return null; } }; const suspensionCache = new Map(); try { const resp = await fetch(userFollowersUrl); if (!resp.ok) throw new Error(`Failed to fetch followers: ${resp.status}`); const data = await resp.json(); let followersUrls = []; if (useNormalFormat) { // Normal Mastodon followers format: array of follower objects if (!Array.isArray(data)) { resultsEl.innerHTML = '<li class="error">Expected an array of follower objects in normal format.</li>'; return; } followersUrls = data.map(follower => follower.url).filter(Boolean); } else { // orderedItems format: object with orderedItems array of follower URLs followersUrls = data.orderedItems || []; } if (followersUrls.length === 0) { resultsEl.innerHTML = '<li>No followers found.</li>'; return; } for (const followerUrl of followersUrls) { const domain = extractDomain(followerUrl); if (!domain) { const li = document.createElement('li'); li.textContent = `Invalid follower URL: ${followerUrl}`; li.classList.add('error'); resultsEl.appendChild(li); continue; } if (!suspensionCache.has(domain)) { suspensionCache.set(domain, checkSuspended(domain, acctToCheck)); } const suspended = await suspensionCache.get(domain); const li = document.createElement('li'); const textNode = document.createTextNode(`${domain} — Suspended: ${suspended === null ? 'Error' : suspended ? 'YES' : 'NO'}`); li.appendChild(textNode); const lookupLink = document.createElement('a'); lookupLink.className = 'lookup-domain'; lookupLink.textContent = `[>> ${domain}]`; lookupLink.href = `https://${domain}/api/v1/accounts/lookup?acct=${encodeURIComponent(acctToCheck)}`; lookupLink.target = '_blank'; lookupLink.rel = 'noopener noreferrer'; li.appendChild(lookupLink); li.classList.add(suspended === null ? 'error' : suspended ? 'suspended' : 'active'); resultsEl.appendChild(li); } } catch (error) { resultsEl.innerHTML = `<li class="error">Error: ${error.message}</li>`; } async function checkSuspended(instance, acct) { const apiUrl = `https://${instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(acct)}`; try { const resp = await fetch(apiUrl); if (resp.status === 404) { return true; } if (!resp.ok) { console.warn(`API error from ${instance}: ${resp.status}`); return null; } const json = await resp.json(); if ('suspended' in json) { return Boolean(json.suspended); } return false; } catch (e) { console.warn(`Error fetching from ${instance}:`, e); return null; } } } document.getElementById('checkForm').addEventListener('submit', event => { event.preventDefault(); runCheck(); }); window.addEventListener('load', () => { runCheck(); }); </script> </body> </html> Fedi Check Discoverability ------------------- <form id="lookup-form"><input type="text" id="domain" name="domain" placeholder="Enter domain" value="mas.to"><input type="text" id="user" name="user" placeholder="Enter username" value="@alceawis@alceawis.com"><button type="button" onclick="openLookup()">Submit</button></form><script>function openLookup() {const domain = document.getElementById('domain').value.trim();const user = document.getElementById('user').value.trim();if (domain && user) {const url = `https://${domain}/api/v1/accounts/lookup?acct=${user}`;window.open(url, '_blank');} else {alert("Please fill out both fields.");}}</script> Logger (Network & Console) ------------------------- <meta charset="UTF-8"> <style> #statusLine { //background: #333; padding: 5px 10px; //border: 1px solid #555; display: flex; justify-content: space-between; align-items: center; margin-top: -20px; cursor: pointer; opacity: 0.8; } #showLogs { font-size: 0.9em; text-decoration: underline; cursor: pointer; color: #66cc66; } #logModal { display: none; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); } #modalContent { background: #333; margin: 5% auto; padding: 20px; border: 1px solid #444; width: 80%; max-height: 70%; overflow-y: auto; white-space: pre-wrap; font-size: 0.9em; color: #ddd; line-height: 1.5; opacity: 1; } #closeModal { float: right; cursor: pointer; font-weight: bold; color: #f8f8f8; } .log-section { margin-bottom: 1em; } a { color: #66ccff; text-decoration: underline; } .log-entry { white-space: pre-wrap; word-wrap: break-word; } #latestLog { font-size: 1.1em; color: #66cc66; } </style> </head> <body> <div id="statusLine"> <span id="latestLog">Waiting for events...</span> </div> <div id="logModal"> <div id="modalContent"> <span id="closeModal">[close]</span> <div class="log-section"><h3>Network Logs</h3><div id="networkLogs"></div></div> <div class="log-section"><h3>Console Logs</h3><div id="consoleLogs"></div></div> </div> </div> <script> const latest = document.getElementById('latestLog'), netDiv = document.getElementById('networkLogs'), conDiv = document.getElementById('consoleLogs'), modal = document.getElementById('logModal'), statusLine = document.getElementById('statusLine'), closeModal = document.getElementById('closeModal'), netHist = [], conHist = [], seen = new Set(); function update(msg) { latest.textContent = msg; } function formatLogWithLinks(text) { const urlRegex = /(https?:\/\/[^\s]+)/g; return text.replace(urlRegex, url => { return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`; }); } console.log = ((log) => (...a) => { const msg = "> " + a.map(x => typeof x === "object" ? JSON.stringify(x) : x).join(" "); conHist.push(msg); update(msg); log.apply(console, a); })(console.log); new PerformanceObserver(list => list.getEntries().forEach(async e => { if (seen.has(e.name)) return; seen.add(e.name); let pct = "100%"; let statusCode = " "; if (e.transferSize && e.encodedBodySize) { let val = Math.min(Math.round((e.transferSize / e.encodedBodySize) * 100), 100); if (!isFinite(val)) val = 100; pct = val + "%"; } try { const response = await fetch(e.name, { method: "HEAD" }); // Use HEAD request to only get headers statusCode = response.status; } catch (error) { console.error('Failed to fetch status code:', error); statusCode = "N/A"; } const msg = "> " + e.name + " - " + pct + " loaded (Status: " + statusCode + ")"; netHist.push(msg); update(msg); })).observe({ type: "resource", buffered: true }); statusLine.onclick = () => { netDiv.innerHTML = netHist.length ? netHist.map(formatLogWithLinks).join('<br>') : '(no network logs yet)'; conDiv.innerHTML = conHist.length ? conHist.map(formatLogWithLinks).join('<br>') : '(no console logs yet)'; modal.style.display = 'block'; }; closeModal.onclick = () => modal.style.display = 'none'; window.onclick = (e) => { if (e.target == modal) modal.style.display = 'none'; }; </script> <br> Repo(s) Compare Tool (GH/CB) ------------------------ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Repo Compare Tool</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } table { border-collapse: collapse; width: 100%; margin-top: 20px; } th, td { border: 1px solid #ccc; padding: 8px 12px; text-align: left; vertical-align: top; } th { background-color: #f9f9f9; } a { color: #0066cc; text-decoration: none; } a:hover { text-decoration: underline; } /* Style for forked repos when checkbox is checked */ .forked a { color: gray !important; font-style: italic; } #log { margin-top: 30px; padding: 10px; background: #f0f0f0; border: 1px solid #ccc; font-size: 14px; max-height: 200px; overflow-y: auto; } #log .log-entry { margin-bottom: 5px; } input { margin-right: 10px; } form label { margin-right: 15px; } </style> </head> <body> <h2>Compare Public Repos</h2> <label><input type="checkbox" id="markForked"> Mark forked repos (grey)</label> <label><input type="checkbox" id="fetchAll"> All repos</label> <form id="repoForm" style="margin-top: 10px;"> <label>URL 1: <input type="text" id="user1" value="https://codeberg.org/alceawisteria" required></label> <label>URL 2: <input type="text" id="user2" value="https://github.com/Ry3yr" required></label> <button type="submit">Compare</button> </form> <h3 id="sharedHeader">Shared Repositories</h3> <table id="sharedReposTable" aria-label="Shared repositories table"> <thead> <tr> <th>Repository Name</th> <!-- This will be replaced dynamically --> </tr> </thead> <tbody> <!-- Shared repos go here --> </tbody> </table> <h3 id="differentHeader">Different Repositories</h3> <table id="differentReposTable" aria-label="Different repositories table"> <thead> <tr> <th id="user1Header">User 1 Only</th> <th id="user2Header">User 2 Only</th> </tr> </thead> <tbody> <!-- Different repos go here --> </tbody> </table> <h3>Debug Log</h3> <div id="log"></div> <script> const logDiv = document.getElementById("log"); function log(msg) { const entry = document.createElement("div"); entry.className = "log-entry"; entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; logDiv.appendChild(entry); logDiv.scrollTop = logDiv.scrollHeight; } function detectPlatform(input) { if (input.includes("codeberg.org")) return "codeberg"; if (input.includes("github.com")) return "github"; return "github"; // default guess } function extractUsername(input) { input = input.trim(); try { if (input.startsWith("http")) { const url = new URL(input); const pathParts = url.pathname.split("/").filter(Boolean); return pathParts[0] || null; } else { return input; } } catch { return input; } } // Fetch all pages if fetchAll is true, otherwise just one page async function fetchRepos(platform, username, fetchAll = false) { let repos = []; let page = 1; const per_page = 100; // max per page for GitHub and probably Codeberg async function fetchPage(page) { let url; if (platform === "github") { url = `https://api.github.com/users/${username}/repos?per_page=${per_page}&page=${page}`; } else if (platform === "codeberg") { url = `https://codeberg.org/api/v1/users/${username}/repos?limit=${per_page}&page=${page}`; } else { log(`Unknown platform for user ${username}`); return []; } log(`Fetching from ${platform}: ${url}`); try { const res = await fetch(url); if (!res.ok) { log(`Failed to fetch from ${platform}: ${res.status} ${res.statusText}`); return []; } const data = await res.json(); log(`Fetched ${data.length} repos from ${platform} for ${username}, page ${page}`); return data.map(repo => ({ name: repo.name, fork: repo.fork })); } catch (err) { log(`Error fetching from ${platform}: ${err.message}`); return []; } } if (!fetchAll) { // Just fetch the first page only repos = await fetchPage(1); } else { // Fetch all pages until empty result while (true) { const pageRepos = await fetchPage(page); if (pageRepos.length === 0) break; repos = repos.concat(pageRepos); if (pageRepos.length < per_page) break; // Last page reached page++; } } return repos; } function makeRepoLink(platform, username, repo, markForked) { let base = platform === "github" ? "https://github.com" : "https://codeberg.org"; const cssClass = markForked && repo.fork ? 'forked' : ''; return `<span class="${cssClass}"><a href="${base}/${username}/${repo.name}" target="_blank" rel="noopener noreferrer">${repo.name}</a></span>`; } document.getElementById("repoForm").addEventListener("submit", async (e) => { e.preventDefault(); logDiv.innerHTML = ''; // Clear log const input1 = document.getElementById("user1").value; const input2 = document.getElementById("user2").value; const markForked = document.getElementById("markForked").checked; const fetchAll = document.getElementById("fetchAll").checked; const platform1 = detectPlatform(input1); const platform2 = detectPlatform(input2); const username1 = extractUsername(input1); const username2 = extractUsername(input2); log(`Starting comparison between "${username1}" (${platform1}) and "${username2}" (${platform2})`); if (fetchAll) log("Fetching ALL repos for each user"); // Update table headers with usernames document.getElementById("sharedHeader").textContent = `Repos shared by ${username1} and ${username2}`; document.getElementById("user1Header").textContent = `Repos only on ${username1}`; document.getElementById("user2Header").textContent = `Repos only on ${username2}`; // Update shared repos table header to two columns for both users document.querySelector("#sharedReposTable thead tr").innerHTML = ` <th>${username1}</th><th>${username2}</th> `; const [repos1, repos2] = await Promise.all([ fetchRepos(platform1, username1, fetchAll), fetchRepos(platform2, username2, fetchAll) ]); // Convert to map for easy lookup by name const map1 = new Map(repos1.map(r => [r.name, r])); const map2 = new Map(repos2.map(r => [r.name, r])); const sameNames = [...map1.keys()].filter(name => map2.has(name)); const only1Names = [...map1.keys()].filter(name => !map2.has(name)); const only2Names = [...map2.keys()].filter(name => !map1.has(name)); // Populate shared repos table (two columns) const sharedTbody = document.querySelector("#sharedReposTable tbody"); if (sameNames.length === 0) { sharedTbody.innerHTML = `<tr><td colspan="2"><em>No shared repositories.</em></td></tr>`; } else { sharedTbody.innerHTML = sameNames .map(name => { const repo1 = map1.get(name); const repo2 = map2.get(name); const link1 = repo1 ? makeRepoLink(platform1, username1, repo1, markForked) : ''; const link2 = repo2 ? makeRepoLink(platform2, username2, repo2, markForked) : ''; return `<tr><td>${link1}</td><td>${link2}</td></tr>`; }) .join(''); } // Populate different repos table with aligned rows const differentTbody = document.querySelector("#differentReposTable tbody"); const maxRows = Math.max(only1Names.length, only2Names.length); let rowsHTML = ''; for (let i = 0; i < maxRows; i++) { const repo1 = only1Names[i] ? makeRepoLink(platform1, username1, map1.get(only1Names[i]), markForked) : ''; const repo2 = only2Names[i] ? makeRepoLink(platform2, username2, map2.get(only2Names[i]), markForked) : ''; rowsHTML += `<tr><td>${repo1}</td><td>${repo2}</td></tr>`; } if (maxRows === 0) { rowsHTML = `<tr><td><em>No unique repositories on either user.</em></td><td></td></tr>`; } differentTbody.innerHTML = rowsHTML; log(`Comparison complete. Shared: ${sameNames.length}, ${username1} only: ${only1Names.length}, ${username2} only: ${only2Names.length}`); }); </script> </body> </html> Moving Button bar with LocStorage position memory -------------------------------------------------------------------------- <style> .marquee-container { width: 100vw; overflow: hidden; box-sizing: border-box; } .marquee { white-space: nowrap; position: relative; will-change: transform; } .marquee a { display: inline-block; margin-right: 0.25rem; } .marquee img { vertical-align: middle; width: 88px; height: 31px; } </style> <div class="marquee-container"> <div class="marquee" id="marquee"> <a href="https://yusaao.org" target="_blank"><img src="https://alceawis.de/other/images/buttons/yusaao.png" alt="yusaao"></a> <a href="https://m.youtube.com/watch?v=Kt4ojxXBIck#https://www.nintendolife.com/news/2025/06/arc-system-works-announces-new-interactive-adventure-for-switch-2" target="_blank"><img src="https://alceawis.de/other/images/buttons/derpy.png" alt="Derpy"></a> <a href="https://web.archive.org/web/20250629060949/https://phoster.zone/buttons/88/piracy.gif" target="_blank" rel="noopener noreferrer"><img src="https://i.ibb.co/27g10Wnn/piracy.gif" alt="Piracy"></a> <a href="https://example.com/browser-support" target="_blank"><img src="https://i.ibb.co/hRR0jL79/anybrowser.gif" alt="Any Browser"></a> <a href="https://alceawis.de/mmdvrmresources.html#https://alcea-wisteria.de/blog/2024/08/mmmd-vrm-vroid-mikumikudance-resources" target="_blank"><img src="https://alceawis.de/other/images/buttons/mikumikuvroid.png" alt="mikumiku"></a> <a href="#" id="reset-marquee">Reset</a> </div> </div> <!--<button id="reset-marquee">Reset</button>--> <script> let position = 0; (function () { const tryInit = () => { const marquee = document.getElementById('marquee'); if (!marquee) { requestAnimationFrame(tryInit); return; } const container = marquee.parentElement; position = parseFloat(localStorage.getItem('marqueePos')) || container.offsetWidth; function animateMarquee() { position -= 0.5; const marqueeWidth = marquee.scrollWidth; if (position < -marqueeWidth) { position = container.offsetWidth; } marquee.style.transform = `translateX(${position}px)`; requestAnimationFrame(animateMarquee); } animateMarquee(); window.addEventListener('beforeunload', () => { localStorage.setItem('marqueePos', position); }); document.getElementById('reset-marquee').addEventListener('click', () => { position = container.offsetWidth; localStorage.setItem('marqueePos', position); marquee.style.transform = `translateX(${position}px)`; }); }; tryInit(); })(); </script> Button Maker ------------ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Upload Image Inside Zone</title> <!-- html2canvas --> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <!-- gif.js --> <script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.min.js"></script> <style> body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; margin-top: 0; touch-action: none; } .button-container { position: relative; width: 300px; height: 81px; background-image: url('https://i.ibb.co/Zp4WJmZX/emptybutton.png'); background-size: 100% 100%; background-repeat: no-repeat; border: 0; margin-bottom: 0; padding: 0; overflow: hidden; } .upload-zone { position: absolute; top: 5%; left: 2%; width: 95%; height: 87%; border: 0px dashed #555; box-sizing: border-box; overflow: hidden; cursor: grab; touch-action: none; background-color: transparent; } .image-wrapper { width: 100%; height: 100%; transform-origin: center center; transition: transform 0.05s ease; will-change: transform; } .image-wrapper img { width: 100%; height: 100%; object-fit: fit; pointer-events: none; user-select: none; touch-action: none; } input[type="file"] { display: none; } #output img { border: 0px solid #ccc; margin-top: 0px; max-width: 100%; } .controls { display: flex; flex-direction: column; align-items: center; gap: 5px; margin-bottom: 10px; } input[type="text"] { padding: 4px; width: 200px; box-sizing: border-box; } button { padding: 6px 12px; } </style> </head> <body> <div class="button-container" id="screenshotTarget"> <label class="upload-zone" title="Click to upload image"> <input type="file" accept="image/*" id="fileInput" /> <div class="image-wrapper" id="imageWrapper"> <img id="uploadedImage" src="" alt="" /> </div> </label> </div> <div class="controls"> <input type="text" id="filenameInput" placeholder="Enter base filename" /> <button id="screenshotBtn">PNG Screenshot</button> <!--<button id="gifBtn">GIF Screenshot</button>--> </div> <div id="output"></div> <script> const fileInput = document.getElementById('fileInput'); const uploadedImage = document.getElementById('uploadedImage'); const imageWrapper = document.getElementById('imageWrapper'); const uploadZone = document.querySelector('.upload-zone'); const screenshotBtn = document.getElementById('screenshotBtn'); const gifBtn = document.getElementById('gifBtn'); const output = document.getElementById('output'); const filenameInput = document.getElementById('filenameInput'); let scale = 1; let lastScale = 1; let startX = 0; let startY = 0; let translateX = 0; let translateY = 0; let initialDistance = null; let isDragging = false; function setTransform() { imageWrapper.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`; } fileInput.addEventListener('change', () => { const file = fileInput.files[0]; if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = e => { uploadedImage.src = e.target.result; scale = 1; lastScale = 1; translateX = 0; translateY = 0; setTransform(); }; reader.readAsDataURL(file); } else { uploadedImage.src = ''; alert('Please upload a valid image file.'); } }); function getDistance(touches) { const dx = touches[0].clientX - touches[1].clientX; const dy = touches[0].clientY - touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); } uploadZone.addEventListener('touchstart', e => { if (e.touches.length === 2) { initialDistance = getDistance(e.touches); } else if (e.touches.length === 1) { isDragging = true; startX = e.touches[0].clientX - translateX; startY = e.touches[0].clientY - translateY; } }); uploadZone.addEventListener('touchmove', e => { e.preventDefault(); if (e.touches.length === 2) { const currentDistance = getDistance(e.touches); const delta = currentDistance / initialDistance; scale = Math.max(1, Math.min(lastScale * delta, 4)); setTransform(); } else if (e.touches.length === 1 && isDragging) { translateX = e.touches[0].clientX - startX; translateY = e.touches[0].clientY - startY; setTransform(); } }); uploadZone.addEventListener('touchend', e => { if (e.touches.length < 2) { lastScale = scale; initialDistance = null; } if (e.touches.length === 0) { isDragging = false; } }); uploadZone.addEventListener('mousedown', e => { isDragging = true; startX = e.clientX - translateX; startY = e.clientY - translateY; uploadZone.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', e => { if (isDragging) { translateX = e.clientX - startX; translateY = e.clientY - startY; setTransform(); } }); document.addEventListener('mouseup', () => { isDragging = false; uploadZone.style.cursor = 'grab'; }); function getFilename(ext) { const base = filenameInput.value.trim() || 'screenshot'; return base + '.' + ext; } screenshotBtn.addEventListener('click', () => { html2canvas(document.getElementById('screenshotTarget'), { useCORS: true, backgroundColor: null, scale: 2 }).then(canvas => { const link = document.createElement('a'); link.download = getFilename('png'); link.href = canvas.toDataURL('image/png'); link.click(); const img = new Image(); img.src = canvas.toDataURL('image/png'); output.innerHTML = ''; output.appendChild(img); }); }); gifBtn.addEventListener('click', () => { html2canvas(document.getElementById('screenshotTarget'), { useCORS: true, backgroundColor: null, scale: 2 }).then(canvas => { const gif = new GIF({ workers: 2, quality: 10, width: canvas.width, height: canvas.height }); gif.addFrame(canvas, { delay: 500 }); gif.on('finished', blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.download = getFilename('gif'); link.href = url; link.click(); const img = new Image(); img.src = url; output.innerHTML = ''; output.appendChild(img); }); gif.render(); }); }); </script> </body> </html> Youtube Channel Video Lister (OAuth) ------------------------------------ <!DOCTYPE html> <html> <head>   <title>YouTube Videos with OAuth Token Input</title>   <script src="https://apis.google.com/js/api.js"></script>   <style>     table { border-collapse: collapse; width: 100%; }     th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }     #tokenInputArea { margin-bottom: 1em; }     #downloadButtons { margin-top: 1em; }     #downloadButtons button { margin-right: 10px; }   </style> </head> <body>   <h1>YouTube Channel Videos</h1>   <div id="tokenInputArea">     <label for="oauthToken">Paste OAuth Access Token:</label><br>     <input type="text" id="oauthToken" size="80" placeholder="Paste your OAuth Access Token here"/>     <button id="submitToken">Submit Token</button>     <button id="clearToken">Clear Token</button>   </div>   <button id="authorize_button" style="display:none;">Authorize with Google</button>   <button id="signout_button" style="display:none;">Sign Out</button>   <div id="content"></div>   <div id="downloadButtons" style="display:none;">     <button id="downloadHtmlBtn">Download Table as HTML</button>     <button id="downloadJsonBtn">Download as JSON</button>   </div>   <script>     const API_KEY = 'YOUR_API_KEY'; // Replace with your API key     const DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest"];     const authorizeButton = document.getElementById('authorize_button');     const signoutButton = document.getElementById('signout_button');     const contentDiv = document.getElementById('content');     const tokenInput = document.getElementById('oauthToken');     const submitTokenBtn = document.getElementById('submitToken');     const clearTokenBtn = document.getElementById('clearToken');     const downloadButtonsDiv = document.getElementById('downloadButtons');     const downloadHtmlBtn = document.getElementById('downloadHtmlBtn');     const downloadJsonBtn = document.getElementById('downloadJsonBtn');     let accessToken = localStorage.getItem('yt_access_token') || null;     let lastVideoDetails = [];     let channelUsername = 'user'; // fallback username     function clearUI() {       contentDiv.innerHTML = '';       authorizeButton.style.display = 'none';       signoutButton.style.display = 'none';       downloadButtonsDiv.style.display = 'none';     }     async function callYouTubeApiWithToken(token) {       contentDiv.innerHTML = 'Loading videos...';       try {         // Get channel info         const channelResponse = await fetch('https://www.googleapis.com/youtube/v3/channels?part=contentDetails,snippet&mine=true', {           headers: {             'Authorization': 'Bearer ' + token           }         });         if (!channelResponse.ok) {           throw new Error('Failed to get channel info. Status: ' + channelResponse.status);         }         const channelData = await channelResponse.json();         if (!channelData.items || channelData.items.length === 0) {           contentDiv.innerHTML = 'No channel found or token has no permission.';           return;         }         // Extract username from snippet title or fallback to 'user'         if (channelData.items[0].snippet && channelData.items[0].snippet.title) {           channelUsername = channelData.items[0].snippet.title.replace(/\s+/g, '_').toLowerCase();         }         const uploadsPlaylistId = channelData.items[0].contentDetails.relatedPlaylists.uploads;         let videos = [];         let nextPageToken = '';         do {           const playlistResponse = await fetch(`https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${uploadsPlaylistId}&maxResults=50&pageToken=${nextPageToken}`, {             headers: {               'Authorization': 'Bearer ' + token             }           });           if (!playlistResponse.ok) {             throw new Error('Failed to get playlist items. Status: ' + playlistResponse.status);           }           const playlistData = await playlistResponse.json();           videos = videos.concat(playlistData.items);           nextPageToken = playlistData.nextPageToken || '';         } while (nextPageToken);         const videoIds = videos.map(v => v.contentDetails.videoId);         let allVideoDetails = [];         for (let i = 0; i < videoIds.length; i += 50) {           const batchIds = videoIds.slice(i, i + 50).join(',');           const videosResponse = await fetch(`https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&id=${batchIds}`, {             headers: {               'Authorization': 'Bearer ' + token             }           });           if (!videosResponse.ok) {             throw new Error('Failed to get video details. Status: ' + videosResponse.status);           }           const videosData = await videosResponse.json();           allVideoDetails = allVideoDetails.concat(videosData.items);         }         lastVideoDetails = allVideoDetails;         renderVideos(allVideoDetails);         document.getElementById('tokenInputArea').style.display = 'none';         signoutButton.style.display = 'inline-block';         downloadButtonsDiv.style.display = 'block';       } catch (error) {         contentDiv.innerHTML = 'Error: ' + error.message;         console.error(error);       }     }     function renderVideos(videoDetails) {       if (!videoDetails.length) {         contentDiv.innerHTML = 'No videos found.';         return;       }       let html = '<table id="videosTable"><thead><tr>';       html += '<th>Title</th><th>Views</th><th>Likes</th><th>Comments</th><th>URL</th><th>Upload Date</th>';       html += '</tr></thead><tbody>';       videoDetails.forEach(video => {         const stats = video.statistics || {};         const snippet = video.snippet || {};         const url = `https://www.youtube.com/watch?v=${video.id}`;         html += '<tr>';         html += `<td>${snippet.title}</td>`;         html += `<td>${stats.viewCount || '0'}</td>`;         html += `<td>${stats.likeCount || '0'}</td>`;         html += `<td>${stats.commentCount || '0'}</td>`;         html += `<td><a href="${url}" target="_blank">Watch</a></td>`;         html += `<td>${new Date(snippet.publishedAt).toLocaleDateString()}</td>`;         html += '</tr>';       });       html += '</tbody></table>';       contentDiv.innerHTML = html;     }     function download(filename, content, type) {       const blob = new Blob([content], { type });       const url = URL.createObjectURL(blob);       const a = document.createElement('a');       a.href = url;       a.download = filename;       document.body.appendChild(a);       a.click();       document.body.removeChild(a);       URL.revokeObjectURL(url);     }     downloadHtmlBtn.addEventListener('click', () => {       const table = document.getElementById('videosTable');       if (!table) {         alert('No table to download.');         return;       }       // Wrap table in minimal HTML so it can be opened standalone       const htmlContent = `<!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>YouTube Videos</title></head> <body> ${table.outerHTML} </body> </html>`;       download('youtube_videos.html', htmlContent, 'text/html');     });     downloadJsonBtn.addEventListener('click', () => {       if (!lastVideoDetails.length) {         alert('No video data to download.');         return;       }       const dateStr = new Date().toISOString().slice(0,10); // yyyy-mm-dd       const filename = `${dateStr}-${channelUsername}.json`;       download(filename, JSON.stringify(lastVideoDetails, null, 2), 'application/json');     });     // On submit token     submitTokenBtn.addEventListener('click', () => {       const token = tokenInput.value.trim();       if (!token) {         alert('Please paste an OAuth access token.');         return;       }       localStorage.setItem('yt_access_token', token);       accessToken = token;       callYouTubeApiWithToken(token);     });     // Clear token     clearTokenBtn.addEventListener('click', () => {       localStorage.removeItem('yt_access_token');       accessToken = null;       contentDiv.innerHTML = '';       document.getElementById('tokenInputArea').style.display = 'block';       signoutButton.style.display = 'none';       downloadButtonsDiv.style.display = 'none';       tokenInput.value = '';     });     // On page load, check for token     if (accessToken) {       tokenInput.value = accessToken;       callYouTubeApiWithToken(accessToken);     } else {       contentDiv.innerHTML = 'Please paste your OAuth access token above and submit.';       document.getElementById('tokenInputArea').style.display = 'block';     }   </script> </body> </html> Basic Reddit client ----------------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Reddit Client</title> <style> body { font-family: Arial, sans-serif; max-width: 900px; margin: 30px auto; color: #222; } input, button { font-size: 1rem; padding: 0.4em; } .post { margin-bottom: 1.5em; } .post img { max-width: 400px; display: block; margin: 0.5em 0; } .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; } .modal-content { background: #fff; padding: 1em; max-width: 800px; width: 90%; max-height: 90%; overflow-y: auto; border-radius: 4px; position: relative; } .close-btn { position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 1.2rem; background: none; border: none; } .comment { border-left: 2px solid #ccc; padding-left: 10px; margin-top: 10px; } </style> </head> <body> <h1>Reddit Client</h1> <form id="subreddit-form"> <label>Subreddit: <input id="subreddit-input" value="deutschland"></label> <button type="submit">Load</button> </form> <div id="posts"></div> <button id="load-more" style="display:none">Load More</button> <div id="modal" class="modal"> <div class="modal-content"> <button class="close-btn">✖</button> <div id="modal-body"></div> </div> </div> <script> const form = document.getElementById('subreddit-form'); const input = document.getElementById('subreddit-input'); const postsDiv = document.getElementById('posts'); const loadMoreBtn = document.getElementById('load-more'); let after = ''; let currentSub = 'deutschland'; form.addEventListener('submit', e => { e.preventDefault(); currentSub = input.value.trim() || 'deutschland'; after = ''; postsDiv.innerHTML = ''; loadMoreBtn.style.display = 'none'; loadPosts(); }); loadMoreBtn.addEventListener('click', loadPosts); async function loadPosts() { const url = `https://www.reddit.com/r/${encodeURIComponent(currentSub)}.json?limit=10&after=${after}`; try { const res = await fetch(url); const json = await res.json(); const children = json.data.children; after = json.data.after || ''; children.forEach(child => renderPost(child.data)); loadMoreBtn.style.display = after ? 'block' : 'none'; } catch (err) { console.error(err); postsDiv.innerHTML += `<p>Error loading subreddit data.</p>`; } } function renderPost(post) { const div = document.createElement('div'); div.className = 'post'; const title = document.createElement('h3'); title.innerHTML = `<a href="#" data-permalink="${post.permalink}">${escapeHtml(post.title)}</a>`; title.querySelector('a').addEventListener('click', openModal); div.appendChild(title); let imgUrl = ''; if (post.preview && post.preview.images?.length) { imgUrl = post.preview.images[0].source.url.replace(/&amp;/g, '&'); } else if (/\.(jpe?g|png|gif)$/i.test(post.url)) { imgUrl = post.url; } if (imgUrl) { const img = document.createElement('img'); img.src = imgUrl; div.appendChild(img); } const snippet = document.createElement('p'); snippet.textContent = post.selftext ? post.selftext.substring(0, 150) + '...' : ''; div.appendChild(snippet); postsDiv.appendChild(div); } const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modal.querySelector('.close-btn').addEventListener('click', () => modal.style.display = 'none'); window.addEventListener('click', e => { if (e.target === modal) modal.style.display = 'none'; }); async function openModal(e) { e.preventDefault(); const permalink = e.target.dataset.permalink; const url = `https://www.reddit.com${permalink}.json`; try { const res = await fetch(url); const json = await res.json(); const post = json[0].data.children[0].data; const comments = json[1].data.children; modalBody.innerHTML = ` <h2><a href="https://www.reddit.com${post.permalink}" target="_blank">${escapeHtml(post.title)}</a></h2> <p>u/${post.author} • ${new Date(post.created_utc*1000).toLocaleString()} • ${post.score} points • ${post.num_comments} comments</p> <div>${decodeHtml(post.selftext_html || '')}</div> ${renderImages(post)} <h3>Comments</h3> `; comments.forEach(c => renderComment(c, 0, permalink)); modal.style.display = 'flex'; } catch (err) { console.error(err); } } function renderImages(post) { const imgs = []; if (/\.(jpe?g|png|gif)$/i.test(post.url)) imgs.push(post.url); if (post.preview?.images) { post.preview.images.forEach(img => { const src = img.source.url.replace(/&amp;/g, '&'); if (!imgs.includes(src)) imgs.push(src); }); } if (post.gallery_data?.items && post.media_metadata) { post.gallery_data.items.forEach(item => { const m = post.media_metadata[item.media_id]?.s?.u; if (m && !imgs.includes(m)) imgs.push(m.replace(/&amp;/g, '&')); }); } return imgs.map(u => `<img src="${u}" style="max-width:100%; margin:10px 0">`).join(''); } function renderComment(node, depth, postPermalink) { if (node.kind !== 't1') return; const c = node.data; const div = document.createElement('div'); div.className = 'comment'; div.style.marginLeft = depth * 20 + 'px'; div.innerHTML = ` <p><strong><a href="https://www.reddit.com/user/${c.author}" target="_blank">u/${c.author}</a></strong> (${c.score} pts) <a href="https://www.reddit.com${postPermalink}${c.id}" target="_blank" style="font-size:0.85em">[link]</a> </p> <div>${decodeHtml(c.body_html)}</div> `; modalBody.appendChild(div); if (c.replies?.data?.children) { c.replies.data.children.forEach(child => renderComment(child, depth + 1, postPermalink)); } } function escapeHtml(s) { return s.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } function decodeHtml(html) { const txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; } // Initial load loadPosts(); </script> </body> </html> Youtube Comments Fetcher ------------------------------------------ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>YouTube Comments Loader</title> <style> .comment { margin-bottom: 1em; border-bottom: 1px solid #ccc; padding: 0.5em; } .reply { margin-left: 2em; color: #555; } button.reply-btn { margin-top: 0.5em; } </style> </head> <body> <h2>YouTube Comments Fetcher</h2> <label> API Key: <input type="text" id="apiKeyInput" placeholder="Enter YouTube API Key"> </label><br><br> <label> Video URL: <input type="text" id="videoUrl" placeholder="Paste YouTube video URL here"> </label><br><br> <button onclick="loadComments()">Load Comments</button> <div id="stats"></div> <div id="comments"></div> <script> function getQueryParam(name) { const params = new URLSearchParams(window.location.search); return params.get(name); } function extractVideoId(url) { const regex = /(?:v=|\.be\/|embed\/)([a-zA-Z0-9_-]{11})/; const match = url.match(regex); return match ? match[1] : null; } async function fetchTotalCommentCount(videoId, apiKey) { const statsUrl = `https://www.googleapis.com/youtube/v3/videos?part=statistics&id=${videoId}&key=${apiKey}`; const res = await fetch(statsUrl); if (!res.ok) throw new Error("Failed to fetch video stats"); const data = await res.json(); return parseInt(data.items[0].statistics.commentCount || "0", 10); } async function loadComments() { const apiKey = document.getElementById('apiKeyInput').value.trim(); const url = document.getElementById('videoUrl').value.trim(); const videoId = extractVideoId(url); if (!apiKey) { alert('API key is required'); return; } if (!videoId) { alert('Invalid YouTube URL'); return; } const statsDiv = document.getElementById('stats'); const commentsDiv = document.getElementById('comments'); statsDiv.innerHTML = ''; commentsDiv.innerHTML = '<p>Loading comments...</p>'; try { const totalComments = await fetchTotalCommentCount(videoId, apiKey); statsDiv.innerHTML = `<h3>Total Comments on Video: ${totalComments}</h3>`; } catch (e) { statsDiv.innerHTML = `<p style="color:red;">Error fetching total comment count</p>`; } let commentsHTML = ''; let pageToken = ''; let totalLoaded = 0; commentsDiv.innerHTML = ''; while (true) { const apiUrl = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${videoId}&key=${apiKey}&maxResults=100&pageToken=${pageToken}`; const response = await fetch(apiUrl); if (!response.ok) { commentsDiv.innerHTML = `<p>Error: ${response.statusText}</p>`; return; } const data = await response.json(); data.items.forEach(item => { const topComment = item.snippet.topLevelComment.snippet; const commentId = item.snippet.topLevelComment.id; const replyCount = item.snippet.totalReplyCount; const commentHTML = ` <div class="comment" id="comment-${commentId}"> <p><strong>${topComment.authorDisplayName}</strong>:</p> <p>${topComment.textDisplay}</p> ${replyCount > 0 ? `<button class="reply-btn" onclick="loadReplies('${commentId}', '${apiKey}')">Load ${replyCount} Replies</button>` : ''} <div class="replies" id="replies-${commentId}"></div> </div> `; commentsDiv.insertAdjacentHTML('beforeend', commentHTML); totalLoaded++; }); if (!data.nextPageToken) break; pageToken = data.nextPageToken; await new Promise(resolve => setTimeout(resolve, 100)); } commentsDiv.insertAdjacentHTML('afterbegin', `<h4>Top-Level Comments Loaded: ${totalLoaded}</h4>`); } async function loadReplies(parentId, apiKey) { const repliesContainer = document.getElementById(`replies-${parentId}`); const button = document.querySelector(`#comment-${parentId} .reply-btn`); if (button) button.disabled = true; let pageToken = ''; let repliesHTML = ''; while (true) { const url = `https://www.googleapis.com/youtube/v3/comments?part=snippet&parentId=${parentId}&key=${apiKey}&maxResults=100&pageToken=${pageToken}`; const response = await fetch(url); if (!response.ok) { repliesContainer.innerHTML = `<p>Error loading replies: ${response.statusText}</p>`; return; } const data = await response.json(); data.items.forEach(reply => { const r = reply.snippet; repliesHTML += ` <div class="reply"> <p><strong>${r.authorDisplayName}</strong>: ${r.textDisplay}</p> </div> `; }); if (!data.nextPageToken) break; pageToken = data.nextPageToken; await new Promise(resolve => setTimeout(resolve, 100)); } repliesContainer.innerHTML = repliesHTML; } window.onload = () => { const apikey = getQueryParam('apikey'); if (apikey) { document.getElementById('apiKeyInput').value = apikey; } }; </script> </body> </html> File to code (box) --------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Multi File Reader</title> </head> <body> <input type="file" id="fileInput" multiple><br><br> <textarea id="output" rows="30" cols="100" readonly></textarea> <script> document.getElementById('fileInput').addEventListener('change', function(event) { const files = event.target.files; const output = document.getElementById('output'); const promises = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const reader = new FileReader(); const promise = new Promise((resolve) => { reader.onload = function(e) { const content = e.target.result; const formatted = `${file.name}\n--\n${content}`; resolve(formatted); }; reader.readAsText(file); }); promises.push(promise); } Promise.all(promises).then(results => { output.value = results.join('\n\n'); }); }); </script> </body> </html> Monthly cost calc --------------------------- <!DOCTYPE html> <html> <head> <title>Monthly Cost Calculator</title> <style> body { font-family: Arial; padding: 20px; } .item { margin-bottom: 10px; } input[type="text"], input[type="number"] { padding: 5px; margin-right: 10px; } button { padding: 5px 10px; margin-top: 10px; } </style> </head> <body> <h2>Monthly Cost Calculator</h2> <div id="items"></div> <button onclick="addItem()">+ Add Item</button> <hr> <h3>Total: €<span id="total">0.00</span></h3> <button onclick="exportJSON()">Export as JSON</button> <script> function addItem(description = '', cost = 0) { const container = document.getElementById('items'); const div = document.createElement('div'); div.className = 'item'; div.innerHTML = ` <input type="text" placeholder="Description" value="${description}"> <input type="number" placeholder="Cost" value="${cost}" oninput="calculateTotal()" step="0.01" min="0"> `; container.appendChild(div); calculateTotal(); } function calculateTotal() { const inputs = document.querySelectorAll('#items input[type="number"]'); let total = 0; inputs.forEach(input => { total += parseFloat(input.value) || 0; }); document.getElementById('total').textContent = total.toFixed(2); } function exportJSON() { const items = []; const rows = document.querySelectorAll('#items .item'); rows.forEach(row => { const desc = row.querySelector('input[type="text"]').value; const cost = parseFloat(row.querySelector('input[type="number"]').value) || 0; items.push({ description: desc, cost: cost }); }); const dataStr = JSON.stringify(items, null, 2); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'monthly_costs.json'; a.click(); URL.revokeObjectURL(url); } function getURLParameter(name) { const urlParams = new URLSearchParams(window.location.search); return urlParams.get(name); } async function loadJSONFromURL() { const jsonUrl = getURLParameter('url'); if (jsonUrl) { try { const response = await fetch(jsonUrl); const data = await response.json(); data.forEach(item => addItem(item.description, item.cost)); } catch (error) { console.error('Failed to load JSON:', error); } } else { addItem(); } } window.onload = loadJSONFromURL; </script> </body> </html> Codeberg _ fetch earliest/lastet commit + patch (email) -------------------------------------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Codeberg Commit Finder</title> <style> body { font-family: sans-serif; margin: 2rem; } input, button { padding: 0.5rem; } input[type="text"] { width: 360px; border: 1px solid #ccc; border-radius: 4px; } button { margin-left: 0.5rem; cursor: pointer; } #result { margin-top: 1.5rem; line-height: 1.5; white-space: pre-wrap; } a { color: #0366d6; margin-right: 1rem; } </style> </head> <body> <h1>Codeberg Commit Finder</h1> <label> Codeberg repo URL or user profile: <input id="repoInput" placeholder="e.g. https://codeberg.org/alceawisteria or /user/repo"> <button id="searchBtn">Find commit</button> </label> <label> <input type="checkbox" id="firstCommitCheck"> Find first-ever commit </label> <div id="result"></div> <script> async function detectRepoFromInput(input) { input = input.trim().replace(/\/+$/, ''); const matchRepo = input.match(/codeberg\.org\/([^\/]+)\/([^\/]+)/i); if (matchRepo) return `${matchRepo[1]}/${matchRepo[2]}`; const matchUser = input.match(/codeberg\.org\/([^\/]+)/i); if (matchUser) { const user = matchUser[1]; const r = await fetch(`https://codeberg.org/api/v1/users/${user}/repos`); if (!r.ok) throw new Error(`Cannot fetch repos for user ${user}`); const repos = await r.json(); if (!repos.length) throw new Error(`User ${user} has no public repos`); return `${user}/${repos[0].name}`; } const fallbackMatch = input.match(/^([\w-]+)\/([\w.-]+)$/); if (fallbackMatch) return input; throw new Error("Please enter a valid Codeberg repo or user profile URL"); } async function fetchCommits(repo, first = false) { const base = `https://codeberg.org/api/v1/repos/${repo}/commits`; let url = `${base}?limit=1&page=${first ? 9999 : 1}`; const res = await fetch(url); if (!res.ok) throw new Error(`Failed to get commits from Codeberg (${res.status})`); const commits = await res.json(); if (!commits.length) throw new Error("No commits found."); return commits[first ? commits.length - 1 : 0]; } document.getElementById('searchBtn').addEventListener('click', async () => { const raw = document.getElementById('repoInput').value; const first = document.getElementById('firstCommitCheck').checked; const result = document.getElementById('result'); result.textContent = 'Searching…'; try { const repo = await detectRepoFromInput(raw); const commit = await fetchCommits(repo, first); const url = `https://codeberg.org/${repo}/commit/${commit.sha}`; const patch = `${url}.patch`; result.innerHTML = ` <strong>Repository:</strong> <a href="https://codeberg.org/${repo}" target="_blank">${repo}</a><br> <strong>Commit SHA:</strong> <a href="${url}" target="_blank">${commit.sha.slice(0, 7)}</a> <a href="${patch}" target="_blank">[.patch]</a><br> <strong>Message:</strong> ${commit.commit.message}<br> <strong>Date:</strong> ${new Date(commit.commit.author.date).toLocaleString()} `; } catch (err) { result.textContent = `⚠️ ${err.message}`; } }); </script> </body> </html> Github _ fetch earliest/lastet commit + patch (email) -------------------------------------------- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>GitHub Commit Finder</title> <style> body { font-family: system-ui, sans-serif; margin: 2rem; } label { display: block; margin-bottom: 0.75rem; } input, button { padding: 0.5rem; } input[type="text"] { width: 360px; border: 1px solid #ccc; border-radius: 4px; } button { margin-left: 0.5rem; cursor: pointer; } #result { margin-top: 1.5rem; line-height: 1.5; white-space: pre-wrap; } a { color: #0366d6; margin-right: 1rem; } </style> </head> <body> <h1>GitHub Commit Finder</h1> <label> GitHub username, profile URL, or repo URL/full-name: <input id="userInput" placeholder="e.g. Ry3yr or https://github.com/Ry3yr/OSTR"> <button id="searchBtn">Find commit</button> </label> <label> <input type="checkbox" id="firstCommitCheck"> Find first-ever commit </label> <div id="result"></div> <script> function extractRepoOrUser(raw) { raw = raw.trim().replace(/\/+$/, ''); // Match full repo URL const repoMatch = raw.match(/github\.com\/([^\/]+)\/([^\/]+)/i); if (repoMatch) return { type: "repo", repo: `${repoMatch[1]}/${repoMatch[2]}` }; // Match full repo format like "Ry3yr/OSTR" const simpleRepoMatch = raw.match(/^([\w-]+)\/([\w.-]+)$/); if (simpleRepoMatch) return { type: "repo", repo: raw }; // Match username or profile URL const userMatch = raw.match(/github\.com\/([^\/]+)/i); return { type: "user", user: userMatch ? userMatch[1] : raw }; } async function fetchLatestPushRepo(user) { const r = await fetch(`https://api.github.com/users/${user}/events/public?per_page=30`); if (!r.ok) throw new Error(`GitHub API returned ${r.status}`); const events = await r.json(); for (const ev of events) { if (ev.type === 'PushEvent') return ev.repo.name; } throw new Error('No recent repositories found.'); } async function fetchLatestCommit(repo) { const r = await fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`); if (!r.ok) throw new Error(`Failed to get latest commit for ${repo}`); const [c] = await r.json(); return formatCommit(c, repo); } async function fetchFirstCommit(repo) { const r1 = await fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`); if (!r1.ok) throw new Error(`Failed to get commit history for ${repo}`); const linkHeader = r1.headers.get("Link"); if (!linkHeader || !linkHeader.includes('rel="last"')) { const [only] = await r1.json(); return formatCommit(only, repo); } const lastPageMatch = linkHeader.match(/<([^>]+)>;\s*rel="last"/); const lastPageUrl = lastPageMatch?.[1]; const rLast = await fetch(lastPageUrl); const commits = await rLast.json(); return formatCommit(commits.at(-1), repo); } function formatCommit(c, repo) { return { repo, sha: c.sha, msg: c.commit.message, date: c.commit.author.date, url: `https://github.com/${repo}/commit/${c.sha}`, patch: `https://github.com/${repo}/commit/${c.sha}.patch` }; } document.getElementById('searchBtn').addEventListener('click', async () => { const input = document.getElementById('userInput').value; const first = document.getElementById('firstCommitCheck').checked; const resultBox = document.getElementById('result'); resultBox.textContent = 'Searching…'; try { const parsed = extractRepoOrUser(input); let repo; if (parsed.type === 'repo') { repo = parsed.repo; } else if (parsed.type === 'user') { repo = await fetchLatestPushRepo(parsed.user); } else { throw new Error('Invalid input.'); } const commit = first ? await fetchFirstCommit(repo) : await fetchLatestCommit(repo); resultBox.innerHTML = ` <strong>Repository:</strong> <a href="https://github.com/${commit.repo}" target="_blank">${commit.repo}</a><br> <strong>Commit SHA:</strong> <a href="${commit.url}" target="_blank">${commit.sha.slice(0, 7)}</a> EMAIL: <a href="${commit.patch}" target="_blank">[.patch]</a><br> <strong>Message:</strong> ${commit.msg}<br> <strong>Date:</strong> ${new Date(commit.date).toLocaleString()} `; } catch (err) { resultBox.textContent = `⚠️ ${err.message}`; } }); </script> </body> </html> Load Website from zip (VirtEnv + Zip FS + ServiceWorker) (complex9) -------------------------------------------------------- V4 (which works off Palapa (offline) Litespeed, but not GH Pages (use V3 there): https://web.archive.org/web/*/https://ry3yr.github.io/zipfs-sw-worker.zip ==zipfs-sw-worker.html== <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>ZIP-FS Demo</title> <style> body { font-family: monospace; white-space: pre-wrap; background: #222; color: #eee; padding: 1rem; } #log { max-height: 30vh; overflow-y: auto; border: 1px solid #555; padding: 1rem; background: #111; } iframe { width: 100%; height: 60vh; border: 2px solid #555; margin-top: 1rem; background: white; color: black; } a.link-root { display: inline-block; margin-top: 1rem; color: #6cf; text-decoration: underline; cursor: pointer; } </style> </head> <body> <h1>ZIP-FS Demo Logs</h1> <pre id="log">Loading logs...\n</pre> <!-- Friendly URL link --> <a href="/root" target="zipfs-iframe" class="link-root">Open Root (/root)</a> <!-- Leave iframe src blank initially --> <iframe id="zipfs-iframe" title="ZIPFS root" name="zipfs-iframe"></iframe> <script> const logEl = document.getElementById('log'); const iframe = document.getElementById('zipfs-iframe'); function log(...args) { const msg = '[ZIPFS] ' + args.join(' '); console.log(msg); logEl.textContent += msg + '\n'; logEl.scrollTop = logEl.scrollHeight; } function loadIframe() { log('Setting iframe src to /root'); iframe.src = '/root'; } if ('serviceWorker' in navigator) { log('Registering Service Worker...'); navigator.serviceWorker.register('/zipfs-sw.js').then(reg => { log('Service Worker registration succeeded:', reg); log('Service Worker state:', reg.active?.state || reg.installing?.state || 'unknown'); if (!navigator.serviceWorker.controller) { log('Page not yet controlled. Reloading in 500ms...'); setTimeout(() => location.reload(), 500); } else { // SW already controlling → safe to load iframe loadIframe(); } navigator.serviceWorker.oncontrollerchange = () => { log('Controller changed. Reloading page...'); location.reload(); }; }).catch(err => { log('Service Worker registration failed:', err); }); navigator.serviceWorker.addEventListener('message', event => { if (event.data?.type === 'SW_LOG') { log(event.data.text); } }); } else { log('Service Workers not supported in this browser.'); } </script> </body> </html> ==zipfs-sw.js== /* zipfs-sw.js */ importScripts('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js'); const ZIP_URL = 'https://ry3yr.github.io/alceawis.de.zip'; const files = new Map(); /* Promise that resolves when the ZIP has finished loading */ let zipReadyResolve; const zipReadyPromise = new Promise(res => { zipReadyResolve = res; }); function swLog(...args) { const msg = '[ZIPFS SW] ' + args.join(' '); console.log(msg); self.clients.matchAll().then(clients => clients.forEach(c => c.postMessage({ type: 'SW_LOG', text: msg })) ); } self.addEventListener('install', () => { swLog('Installing…'); self.skipWaiting(); }); self.addEventListener('activate', event => { swLog('Activating → downloading ZIP…'); event.waitUntil( (async () => { const r = await fetch(ZIP_URL, { mode: 'cors' }); swLog('ZIP fetch status:', r.status); if (!r.ok) throw new Error('ZIP fetch failed'); const buf = await r.arrayBuffer(); swLog('Unzipping…'); const zip = await JSZip.loadAsync(buf); let count = 0; for (const entry of Object.values(zip.files)) { if (entry.dir) continue; const path = '/' + entry.name.split('/').slice(1).join('/'); const content = await entry.async('uint8array'); files.set(path, content); swLog('→', path, `(${content.length} bytes)`); count++; } swLog('Unzip done –', count, 'files'); zipReadyResolve(); await self.clients.claim(); })().catch(err => swLog('ZIP load failed:', err)) ); }); self.addEventListener('fetch', event => { const url = new URL(event.request.url); /* 1️⃣ Let cross‑origin requests go to the network untouched */ if (url.origin !== self.location.origin) return; /* 2️⃣ Friendly URLs: “/”, “/root”, “/home” → “/index.html” */ let path = url.pathname; if (path === '/' || path === '/root' || path === '/home') path = '/index.html'; event.respondWith( (async () => { /* Wait until the ZIP is ready */ await zipReadyPromise; const file = files.get(path); if (file) { swLog('Serving', path, 'from ZIP'); return new Response(file, { headers: { 'Content-Type': guessType(path) } }); } /* 3️⃣ Same‑origin file not in ZIP → fall back to network */ swLog('Not in ZIP, fetching from network:', path); try { return await fetch(event.request); } catch (err) { swLog('Network fetch failed:', err); return new Response('<h1>Offline & not cached</h1>', { headers: { 'Content-Type': 'text/html' }, status : 503 }); } })() ); }); function guessType(p) { return p.endsWith('.html') ? 'text/html' : p.endsWith('.js') ? 'application/javascript': p.endsWith('.css') ? 'text/css' : p.endsWith('.json') ? 'application/json' : p.endsWith('.png') ? 'image/png' : p.endsWith('.jpg')||p.endsWith('.jpeg') ? 'image/jpeg' : p.endsWith('.svg') ? 'image/svg+xml' : 'application/octet-stream'; } /* Optional error logging */ self.addEventListener('error', e => swLog('Error:', e.message)); self.addEventListener('unhandledrejection',e => swLog('Unhandled rejection:', e.reason)); Wage Calc / Lohnrechner --------------------------------------- <!DOCTYPE html> <html lang="de"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body { font-family: Arial, sans-serif; padding: 20px; background-color: #f4f4f4; } .container { max-width: 450px; margin: auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } label { display: block; margin-top: 15px; } input { width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box; } #bruttoMonth, #nettoMonth { font-weight: bold; } .result { color: green; } </style> </head> <body> <div class="container"> <h2>WageCalc</h2> <label for="hourlyWage">Brutto-Stundenlohn (€):</label> <input type="number" id="hourlyWage" placeholder="z. B. 22" step="0.01"> <label for="hoursPerDay">Stunden pro Tag:</label> <input type="number" id="hoursPerDay" value="8" placeholder="z. B. 8"> <label for="daysPerWeek">Tage pro Woche:</label> <input type="number" id="daysPerWeek" value="5" placeholder="z. B. 5"> <label for="weeksPerMonth">Wochen pro Monat (Standard: 4.33):</label> <input type="number" id="weeksPerMonth" value="4" placeholder="z. B. 4.33" step="0.01"> <label for="taxPercent">Steuerabzug in % (Standard: 25):</label> <input type="number" id="taxPercent" placeholder="z. B. 25" step="0.1" value="25"> <label for="bruttoMonth">Brutto-Monatslohn (€):</label> <input type="number" id="bruttoMonth" placeholder="Wird automatisch berechnet" step="0.01"> <label for="nettoMonth">Netto-Monatslohn (€):</label> <input type="text" id="nettoMonth" readonly class="result"> </div> <script> const hourlyWageEl = document.getElementById("hourlyWage"); const hoursPerDayEl = document.getElementById("hoursPerDay"); const daysPerWeekEl = document.getElementById("daysPerWeek"); const weeksPerMonthEl = document.getElementById("weeksPerMonth"); const taxPercentEl = document.getElementById("taxPercent"); const bruttoMonthEl = document.getElementById("bruttoMonth"); const nettoMonthEl = document.getElementById("nettoMonth"); let isEditingBrutto = false; function getValue(el, defaultValue = 0) { const val = parseFloat(el.value); return isNaN(val) ? defaultValue : val; } function updateNetto(brutto) { const taxPercent = getValue(taxPercentEl, 25); const netto = brutto * (1 - (taxPercent / 100)); nettoMonthEl.value = netto.toFixed(2) + " €"; } function calculateBrutto() { if (isEditingBrutto) return; const wage = getValue(hourlyWageEl); const hours = getValue(hoursPerDayEl); const days = getValue(daysPerWeekEl); const weeks = getValue(weeksPerMonthEl, 4.33); const brutto = wage * hours * days * weeks; bruttoMonthEl.value = brutto.toFixed(2); updateNetto(brutto); } function reverseCalculateHourlyWage() { const brutto = getValue(bruttoMonthEl); const hours = getValue(hoursPerDayEl); const days = getValue(daysPerWeekEl); const weeks = getValue(weeksPerMonthEl, 4.33); const totalHours = hours * days * weeks; if (totalHours > 0) { const newWage = brutto / totalHours; hourlyWageEl.value = newWage.toFixed(2); } updateNetto(brutto); } [hourlyWageEl, hoursPerDayEl, daysPerWeekEl, weeksPerMonthEl, taxPercentEl].forEach(input => { input.addEventListener("input", () => { isEditingBrutto = false; calculateBrutto(); }); }); bruttoMonthEl.addEventListener("input", () => { isEditingBrutto = true; reverseCalculateHourlyWage(); }); </script> </body> </html> </plaintext>