From c4d24aa88b4189cded52b5d8d931c63bcbbb7201 Mon Sep 17 00:00:00 2001 From: AvaLilac Date: Thu, 26 Feb 2026 06:48:01 -0500 Subject: [PATCH] Adds Faviorite Gifs/Links/videos support to Stoat Signed-off-by: AvaLilac --- src/aviafavsystem.js | 327 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 src/aviafavsystem.js diff --git a/src/aviafavsystem.js b/src/aviafavsystem.js new file mode 100644 index 0000000..252846c --- /dev/null +++ b/src/aviafavsystem.js @@ -0,0 +1,327 @@ +(function () { + +if (window.__AVIA_FAVORITES_LOADED__) return; +window.__AVIA_FAVORITES_LOADED__ = true; + +const STORAGE_KEY = "avia_favorites"; + +const getFavorites = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); +const setFavorites = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + +function extractYouTubeID(url) { + const reg = /(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^&?/]+)/; + const match = url.match(reg); + return match ? match[1] : null; +} + +function toggleFavoritesPanel() { + + let panel = document.getElementById("avia-favorites-panel"); + if (panel) { + panel.style.display = panel.style.display === "none" ? "flex" : "none"; + return; + } + + panel = document.createElement("div"); + panel.id = "avia-favorites-panel"; + + Object.assign(panel.style, { + position: "fixed", + bottom: "40px", + right: "40px", + width: "640px", + height: "580px", + background: "#1e1e1e", + color: "#fff", + borderRadius: "20px", + boxShadow: "0 12px 35px rgba(0,0,0,0.45)", + zIndex: 999999, + display: "flex", + flexDirection: "column", + overflow: "hidden", + border: "1px solid rgba(255,255,255,0.08)" + }); + + const header = document.createElement("div"); + header.textContent = "Favorites"; + Object.assign(header.style, { + padding: "18px", + fontWeight: "600", + fontSize: "16px", + background: "rgba(255,255,255,0.04)", + borderBottom: "1px solid rgba(255,255,255,0.08)", + cursor: "move", + position: "relative", + userSelect: "none" + }); + + const close = document.createElement("div"); + close.textContent = "✕"; + Object.assign(close.style, { + position: "absolute", + right: "18px", + top: "16px", + cursor: "pointer" + }); + close.onclick = () => panel.style.display = "none"; + header.appendChild(close); + + const inputRow = document.createElement("div"); + Object.assign(inputRow.style, { + display: "flex", + gap: "8px", + padding: "14px 18px" + }); + + const urlInput = document.createElement("input"); + urlInput.placeholder = "Paste link..."; + Object.assign(urlInput.style, { + flex: "2", + padding: "10px", + borderRadius: "10px", + border: "none", + outline: "none" + }); + + const titleInput = document.createElement("input"); + titleInput.placeholder = "Optional title..."; + Object.assign(titleInput.style, { + flex: "1", + padding: "10px", + borderRadius: "10px", + border: "none", + outline: "none" + }); + + const addBtn = document.createElement("button"); + addBtn.textContent = "Add"; + Object.assign(addBtn.style, { + padding: "10px 16px", + borderRadius: "10px", + border: "none", + cursor: "pointer" + }); + + inputRow.appendChild(urlInput); + inputRow.appendChild(titleInput); + inputRow.appendChild(addBtn); + + const grid = document.createElement("div"); + Object.assign(grid.style, { + flex: "1", + minHeight: "0", + overflowY: "auto", + padding: "18px", + display: "grid", + gridTemplateColumns: "repeat(auto-fill, 120px)", + gap: "14px", + alignContent: "start" + }); + + panel.appendChild(header); + panel.appendChild(inputRow); + panel.appendChild(grid); + document.body.appendChild(panel); + + let isDragging = false, offsetX, offsetY; + + header.addEventListener("mousedown", e => { + isDragging = true; + offsetX = e.clientX - panel.offsetLeft; + offsetY = e.clientY - panel.offsetTop; + }); + + document.addEventListener("mouseup", () => isDragging = false); + + document.addEventListener("mousemove", e => { + if (!isDragging) return; + panel.style.left = (e.clientX - offsetX) + "px"; + panel.style.top = (e.clientY - offsetY) + "px"; + panel.style.right = "auto"; + panel.style.bottom = "auto"; + }); + + function showToast(card) { + const toast = document.createElement("div"); + toast.textContent = "Copied to clipboard"; + Object.assign(toast.style, { + position: "absolute", + bottom: "6px", + left: "50%", + transform: "translateX(-50%)", + background: "rgba(0,0,0,0.85)", + padding: "6px 10px", + borderRadius: "8px", + fontSize: "11px", + opacity: "0", + transition: "opacity 0.2s", + pointerEvents: "none" + }); + card.appendChild(toast); + requestAnimationFrame(() => toast.style.opacity = "1"); + setTimeout(() => { + toast.style.opacity = "0"; + setTimeout(() => toast.remove(), 200); + }, 2000); + } + + function render() { + + grid.innerHTML = ""; + const favorites = getFavorites(); + + favorites.forEach(item => { + + const card = document.createElement("div"); + Object.assign(card.style, { + position: "relative", + width: "120px", + height: "120px", + borderRadius: "14px", + overflow: "hidden", + background: "rgba(255,255,255,0.05)", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center" + }); + + const remove = document.createElement("div"); + remove.textContent = "✕"; + Object.assign(remove.style, { + position: "absolute", + top: "6px", + right: "8px", + fontSize: "12px", + cursor: "pointer", + background: "rgba(0,0,0,0.6)", + padding: "2px 6px", + borderRadius: "6px", + zIndex: 2 + }); + + remove.onclick = (e) => { + e.stopPropagation(); + setFavorites(favorites.filter(f => f.url !== item.url)); + render(); + }; + + card.appendChild(remove); + + let mediaAdded = false; + + const ytID = extractYouTubeID(item.url); + if (ytID) { + const img = new Image(); + img.src = `https://img.youtube.com/vi/${ytID}/hqdefault.jpg`; + Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" }); + card.appendChild(img); + mediaAdded = true; + } + + if (!mediaAdded) { + const ext = item.url.split(".").pop().split("?")[0].toLowerCase(); + const isVideo = ["mp4","webm","mov","gifv"].includes(ext); + + if (isVideo) { + const video = document.createElement("video"); + video.src = item.url.replace(".gifv",".mp4"); + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + Object.assign(video.style, { width:"100%", height:"100%", objectFit:"cover" }); + video.onerror = fallback; + card.appendChild(video); + } else { + const img = new Image(); + img.src = item.url; + Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" }); + img.onerror = fallback; + card.appendChild(img); + } + } + + function fallback() { + card.innerHTML = ""; + card.appendChild(remove); + const text = document.createElement("div"); + text.textContent = item.title || item.url; + Object.assign(text.style, { + padding:"8px", + fontSize:"11px", + textAlign:"center", + wordBreak:"break-word" + }); + card.appendChild(text); + } + + if (item.title) { + const titleOverlay = document.createElement("div"); + titleOverlay.textContent = item.title; + Object.assign(titleOverlay.style, { + position:"absolute", + bottom:"0", + width:"100%", + background:"rgba(0,0,0,0.6)", + fontSize:"11px", + padding:"4px", + textAlign:"center", + whiteSpace:"nowrap", + overflow:"hidden", + textOverflow:"ellipsis" + }); + card.appendChild(titleOverlay); + } + + card.onclick = async () => { + await navigator.clipboard.writeText(item.url); + showToast(card); + }; + + grid.appendChild(card); + }); + } + + addBtn.onclick = () => { + const url = urlInput.value.trim(); + const title = titleInput.value.trim(); + if (!url) return; + const favorites = getFavorites(); + if (favorites.some(f => f.url === url)) return; + favorites.push({ url, title, addedAt: Date.now() }); + setFavorites(favorites); + urlInput.value = ""; + titleInput.value = ""; + render(); + }; + + render(); +} + +function injectButton() { + + if (document.getElementById("avia-favorites-btn")) return; + + const gifSpan = [...document.querySelectorAll("span.material-symbols-outlined")] + .find(s => s.textContent.trim() === "gif"); + + if (!gifSpan) return; + + const wrapper = gifSpan.closest("div.flex-sh_0"); + if (!wrapper) return; + + const clone = wrapper.cloneNode(true); + clone.id = "avia-favorites-btn"; + clone.querySelector("span.material-symbols-outlined").textContent = "star"; + clone.querySelector("button").onclick = toggleFavoritesPanel; + + wrapper.parentElement.insertBefore(clone, wrapper.nextSibling); +} + +new MutationObserver(injectButton) +.observe(document.body, { childList: true, subtree: true }); + +injectButton(); + +})(); \ No newline at end of file