Skip to content

[ FEATURE REQUEST ] - Enable GIF Copy-Paste Support (Like Emojis) in AppLauncher #150

@AstroJr0

Description

@AstroJr0

Feature Request:

Hey there, It would be real fun allowing users to copy and paste GIFs (e.g. from Tenor.com or Giphy) directly from the AppLauncher. And maybe preview or auto-upload GIFs to ensure persistence

or just show the gif in the app launcher as preview, on click

  • just copy the url of that gif (as a jpeg by default).
  • by doing right click, copy as format such as HD GIF, SD GIF, MP4 etc.

Idk why but, i felt like it... And acn be a help for some api and bento-grid thingies
heres the example html file:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GIF LAUNCHER CONCEPT</title>
    <link
        href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Syne:wght@500;600&display=swap"
        rel="stylesheet" />
    <style>
        *,
        *::before,
        *::after {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        :root {
            --bg: #0e0e0f;
            --surface: rgba(22, 22, 26, 0.97);
            --border: rgba(255, 255, 255, 0.09);
            --border-hover: rgba(52, 211, 195, 0.55);
            --accent: #34d3c3;
            --accent-dim: rgba(52, 211, 195, 0.12);
            --text: #e8e8ea;
            --text-muted: rgba(232, 232, 234, 0.35);
            --radius: 14px;
            --launcher-w: 700px;
        }

        body {
            background: var(--bg);
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: "JetBrains Mono", monospace;
            color: var(--text);
        }

        /*  idk why tf i made these  */
        .launcher {
            width: var(--launcher-w);
            background: var(--surface);
            border-radius: 22px;
            border: 1.5px solid var(--border);
            overflow: hidden;
            box-shadow:
                0 0 0 1px rgba(255, 255, 255, 0.03),
                0 32px 80px rgba(0, 0, 0, 0.7);
        }

        /*  search bar!!  */
        .search-area {
            padding: 14px 14px 10px;
            display: flex;
            align-items: center;
            gap: 10px;
            border-bottom: 1px solid var(--border);
        }

        .search-icon {
            flex-shrink: 0;
            width: 18px;
            height: 18px;
            opacity: 0.35;
            fill: var(--text);
        }

        .search-input {
            flex: 1;
            background: transparent;
            border: none;
            outline: none;
            font-family: "JetBrains Mono", monospace;
            font-size: 14px;
            color: var(--text);
            caret-color: var(--accent);
        }

        .search-input::placeholder {
            color: var(--text-muted);
        }

        .tag-pill {
            font-size: 10px;
            color: var(--accent);
            background: var(--accent-dim);
            border: 1px solid rgba(52, 211, 195, 0.2);
            border-radius: 6px;
            padding: 3px 8px;
            letter-spacing: 0.05em;
            font-family: "JetBrains Mono", monospace;
            white-space: nowrap;
            opacity: 0;
            transition: opacity 0.2s;
        }

        .tag-pill.visible {
            opacity: 1;
        }

        /*  bento grid [tis shyt looks hella cool] */
        .bento-grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            grid-auto-rows: 130px;
            gap: 7px;
            padding: 10px;
            max-height: 530px;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
        }

        .bento-grid::-webkit-scrollbar {
            width: 4px;
        }

        .bento-grid::-webkit-scrollbar-track {
            background: transparent;
        }

        .bento-grid::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.08);
            border-radius: 4px;
        }

        /*  gif cells  */
        .gif-cell {
            border-radius: var(--radius);
            overflow: hidden;
            position: relative;
            cursor: pointer;
            border: 1.5px solid var(--border);
            transition:
                border-color 0.15s ease,
                transform 0.12s ease;
            background: rgba(255, 255, 255, 0.03);
            outline: none;
        }

        .gif-cell:focus-visible {
            border-color: var(--accent);
        }

        .gif-cell:hover {
            border-color: var(--border-hover);
            transform: scale(1.025);
            z-index: 2;
        }

        .gif-cell img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
            pointer-events: none;
        }

        .gif-cell.wide {
            grid-column: span 2;
        }

        .gif-cell.tall {
            grid-row: span 2;
        }

        .gif-cell.large {
            grid-column: span 2;
            grid-row: span 2;
        }

        /* copy flash overlay */
        .gif-cell .copy-flash {
            position: absolute;
            inset: 0;
            background: rgba(52, 211, 195, 0.22);
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            transition: opacity 0.08s;
            pointer-events: none;
            border-radius: calc(var(--radius) - 2px);
        }

        .gif-cell .copy-flash.show {
            opacity: 1;
        }

        .copy-flash-label {
            font-size: 11px;
            font-family: "JetBrains Mono", monospace;
            color: #fff;
            background: rgba(0, 0, 0, 0.55);
            padding: 4px 10px;
            border-radius: 6px;
            letter-spacing: 0.04em;
        }

        /* hover label */
        .gif-cell .hover-label {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 20px 8px 7px;
            background: linear-gradient(to top, rgba(0, 0, 0, 0.65), transparent);
            font-size: 10px;
            color: rgba(255, 255, 255, 0.6);
            opacity: 0;
            transition: opacity 0.15s;
            pointer-events: none;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .gif-cell:hover .hover-label {
            opacity: 1;
        }

        .skeleton {
            animation: shimmer 1.5s ease-in-out infinite;
            background: linear-gradient(90deg,
                    rgba(255, 255, 255, 0.03) 0%,
                    rgba(255, 255, 255, 0.07) 50%,
                    rgba(255, 255, 255, 0.03) 100%);
            background-size: 200% 100%;
        }

        @keyframes shimmer {
            0% {
                background-position: 200% 0;
            }

            100% {
                background-position: -200% 0;
            }
        }

        /* for   empty / error states  */
        .state-box {
            grid-column: 1 / -1;
            grid-row: 1 / span 3;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 10px;
            padding: 48px 24px;
            color: var(--text-muted);
        }

        .state-glyph {
            font-size: 36px;
            font-family: "Syne", sans-serif;
            opacity: 0.25;
        }

        .state-label {
            font-size: 12px;
            letter-spacing: 0.06em;
        }

        /*  status bar  */
        .status-bar {
            padding: 8px 14px;
            border-top: 1px solid var(--border);
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 11px;
            color: var(--text-muted);
        }

        .kbd {
            display: inline-flex;
            align-items: center;
            gap: 4px;
        }

        .key {
            font-size: 10px;
            color: var(--accent);
            background: var(--accent-dim);
            border: 1px solid rgba(52, 211, 195, 0.2);
            border-radius: 4px;
            padding: 1px 6px;
            font-family: "JetBrains Mono", monospace;
        }

        /*  context menu  */
        .ctx-menu {
            position: fixed;
            background: rgba(16, 16, 18, 0.98);
            border: 1.5px solid rgba(255, 255, 255, 0.12);
            border-radius: 12px;
            overflow: hidden;
            z-index: 9999;
            min-width: 175px;
            display: none;
            box-shadow: 0 16px 40px rgba(0, 0, 0, 0.7);
        }

        .ctx-menu.open {
            display: block;
            animation: popIn 0.1s ease;
        }

        @keyframes popIn {
            from {
                transform: scale(0.94);
                opacity: 0;
            }

            to {
                transform: scale(1);
                opacity: 1;
            }
        }

        .ctx-header {
            padding: 8px 14px 6px;
            font-size: 10px;
            color: rgba(52, 211, 195, 0.6);
            letter-spacing: 0.08em;
            border-bottom: 1px solid rgba(255, 255, 255, 0.07);
        }

        .ctx-item {
            padding: 10px 14px;
            color: rgba(232, 232, 234, 0.85);
            font-size: 12px;
            font-family: "JetBrains Mono", monospace;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 10px;
            transition: background 0.08s;
        }

        .ctx-item:hover {
            background: rgba(52, 211, 195, 0.1);
            color: var(--text);
        }

        .ctx-item+.ctx-item {
            border-top: 1px solid rgba(255, 255, 255, 0.04);
        }

        .ctx-dot {
            width: 7px;
            height: 7px;
            border-radius: 50%;
            flex-shrink: 0;
        }
    </style>
</head>

<body>
    <div class="launcher" id="launcher">
        <!-- SEARCH BARRRRR-->
        <div class="search-area">
            <svg class="search-icon" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                <path
                    d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11zM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9z" />
            </svg>
            <input class="search-input" id="searchInput" type="text" placeholder="Search GIFs..." autocomplete="off"
                autofocus />
            <span class="tag-pill" id="tagPill">tenor</span>
        </div>

        <!-- Gridz fritz-->
        <div class="bento-grid" id="bentoGrid">
            <div class="state-box">
                <div class="state-glyph">GIF</div>
                <div class="state-label">type to search tenor</div>
            </div>
        </div>

        <!-- Status bar -->
        <div class="status-bar">
            <span id="statusMsg">powered by tenor</span>
            <div class="kbd">
                <span class="key">Enter</span> copy ·
                <span class="key">RClick</span> format
            </div>
        </div>
    </div>

    <!-- Context menuu -->
    <div class="ctx-menu" id="ctxMenu">
        <div class="ctx-header">COPY AS</div>
        <div class="ctx-item" data-fmt="jpeg">
            <span class="ctx-dot" style="background: #f59e0b"></span> JPEG frame
        </div>
        <div class="ctx-item" data-fmt="png">
            <span class="ctx-dot" style="background: #3b82f6"></span> PNG frame
        </div>
        <div class="ctx-item" data-fmt="webp">
            <span class="ctx-dot" style="background: #a855f7"></span> WebP frame
        </div>
        <div class="ctx-item" data-fmt="url">
            <span class="ctx-dot" style="background: #34d3c3"></span> GIF URL (text)
        </div>
    </div>

    <script lang="ts">
        const TENOR_KEY = "LIVDSRZULELA"; // found this usable public key
        const TENOR_BASE = "https://api.tenor.com/v1/search";

        const grid = document.getElementById("bentoGrid");
        const input = document.getElementById("searchInput");
        const ctxMenu = document.getElementById("ctxMenu");
        const statusMsg = document.getElementById("statusMsg");
        const tagPill = document.getElementById("tagPill");

        let gifs = [];
        let activeIdx = -1;
        let debounce;

        /*  Bento layout pattern (cycles through 20 slots)  */
        const PATTERN = [
            "large",
            "",
            "",
            "tall",
            "",
            "wide",
            "",
            "",
            "large",
            "",
            "",
            "",
            "wide",
            "",
            "",
            "",
            "tall",
            "",
            "",
            "",
        ];

        /*  Search  */
        input.addEventListener("input", () => {
            clearTimeout(debounce);
            const q = input.value.trim();
            tagPill.className = q ? "tag-pill visible" : "tag-pill";
            if (!q) {
                showEmpty();
                return;
            }
            debounce = setTimeout(() => search(q), 380);
        });

        input.addEventListener("keydown", (e) => {
            if (e.key === "Escape") {
                input.value = "";
                showEmpty();
                tagPill.className = "tag-pill";
            }
            if (e.key === "Enter" && activeIdx >= 0) copyGif(activeIdx, "jpeg");
        });

        async function search(q) {
            showSkeleton();
            try {
                const url = `${TENOR_BASE}?q=${encodeURIComponent(q)}&key=${TENOR_KEY}&limit=20&media_filter=minimal`;
                const r = await fetch(url);
                const d = await r.json();
                if (!d.results?.length) {
                    showNoResults();
                    return;
                }
                gifs = d.results.map((g) => ({
                    gif: g.media[0]?.gif?.url || "",
                    tiny: g.media[0]?.tinygif?.url || g.media[0]?.gif?.url || "",
                    title: g.title || "",
                }));
                renderGrid();
                statusMsg.textContent = `${gifs.length} results for "${q}"`;
            } catch {
                showError();
            }
        }

        /*  Render bento grid  */
        function renderGrid() {
            grid.innerHTML = "";
            gifs.forEach((g, i) => {
                const cls = PATTERN[i % PATTERN.length] || "";
                const cell = document.createElement("div");
                cell.className = "gif-cell" + (cls ? " " + cls : "");
                cell.tabIndex = 0;

                const img = document.createElement("img");
                img.src = g.tiny;
                img.alt = g.title;
                img.loading = "lazy";

                const flash = document.createElement("div");
                flash.className = "copy-flash";
                flash.innerHTML = '<span class="copy-flash-label">Copied!</span>';

                const label = document.createElement("div");
                label.className = "hover-label";
                label.textContent = g.title;

                cell.append(img, flash, label);

                cell.addEventListener("click", () => {
                    activeIdx = i;
                    copyGif(i, "jpeg");
                });
                cell.addEventListener("contextmenu", (e) => {
                    e.preventDefault();
                    activeIdx = i;
                    openCtx(e.clientX, e.clientY);
                });
                cell.addEventListener("keydown", (e) => {
                    if (e.key === "Enter") {
                        activeIdx = i;
                        copyGif(i, "jpeg");
                    }
                });

                grid.appendChild(cell);
            });
        }

        async function copyGif(idx, fmt) {
            const g = gifs[idx];
            if (!g) return;

            if (fmt === "url") {
                await navigator.clipboard.writeText(g.gif).catch(() => { });
                flashCell(idx);
                statusMsg.textContent = "Copied GIF URL";
                return;
            }

            /* canvas frame grab */
            const mimeMap = {
                jpeg: "image/jpeg",
                png: "image/png",
                webp: "image/webp",
                /* available formats here*/
            };
            const mime = mimeMap[fmt] || "image/jpeg";

            try {
                const img = new Image();
                img.crossOrigin = "anonymous";

                const proxyUrl = `https://images.weserv.nl/?url=${encodeURIComponent(g.tiny)}&output=${fmt}&maxage=1d`;
                img.src = proxyUrl;

                await new Promise((res, rej) => {
                    img.onload = res;
                    img.onerror = rej;
                });

                const canvas = document.createElement("canvas");
                canvas.width = img.naturalWidth || 320;
                canvas.height = img.naturalHeight || 240;
                canvas.getContext("2d").drawImage(img, 0, 0);

                const blob = await new Promise((res) =>
                    canvas.toBlob(res, mime, 0.92),
                );

                await navigator.clipboard.write([
                    new ClipboardItem({ [mime]: blob }),
                ]);
                flashCell(idx);
                statusMsg.textContent = `Copied as ${fmt.toUpperCase()}`;
            } catch {
                /* fallback: just copy da url */
                try {
                    await navigator.clipboard.writeText(g.gif);
                } catch { }
                flashCell(idx);
                statusMsg.textContent = `Copied URL (direct format blocked by browser)`;
            }
        }

        function flashCell(idx) {
            const cells = grid.querySelectorAll(".gif-cell");
            const c = cells[idx];
            if (!c) return;
            const f = c.querySelector(".copy-flash");
            f.classList.add("show");
            setTimeout(() => f.classList.remove("show"), 700);
        }

        /*  context menu  */
        function openCtx(x, y) {
            ctxMenu.style.left = Math.min(x, window.innerWidth - 185) + "px";
            ctxMenu.style.top = Math.min(y, window.innerHeight - 170) + "px";
            ctxMenu.className = "ctx-menu open";
        }

        ctxMenu.querySelectorAll(".ctx-item").forEach((el) => {
            el.addEventListener("click", () => {
                ctxMenu.className = "ctx-menu";
                copyGif(activeIdx, el.dataset.fmt);
            });
        });

        document.addEventListener(
            "click",
            () => (ctxMenu.className = "ctx-menu"),
        );
        document.addEventListener("keydown", (e) => {
            if (e.key === "Escape") ctxMenu.className = "ctx-menu";
        });

        /*  state helpers  */
        function showSkeleton() {
            grid.innerHTML = "";
            const sizes = ["large", "", "", "", "wide", "", "", "", "", "", "", ""];
            sizes.forEach((s) => {
                const d = document.createElement("div");
                d.className = "gif-cell skeleton" + (s ? " " + s : "");
                grid.appendChild(d);
            });
            statusMsg.textContent = "Searching…";
        }

        function showEmpty() {
            gifs = [];
            activeIdx = -1;
            grid.innerHTML = `
    <div class="state-box">
      <div class="state-glyph">GIF</div>
      <div class="state-label">type to search tenor</div>
    </div>`;
            statusMsg.textContent = "powered by tenor";
        }

        function showNoResults() {
            grid.innerHTML = `
    <div class="state-box">
      <div class="state-glyph">∅</div>
      <div class="state-label">no GIFs found</div>
    </div>`;
            statusMsg.textContent = "no results";
        }

        function showError() {
            grid.innerHTML = `
    <div class="state-box">
      <div class="state-glyph">!</div>
      <div class="state-label">could not reach tenor</div>
    </div>`;
            statusMsg.textContent = "network error";
        }
    </script>
</body>

</html>```

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions