EzGif.com – True Menu [Ath]

Complete menu with all tools on Ez Gif (EzGif.com).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           EzGif.com – True Menu [Ath]
// @description    Complete menu with all tools on Ez Gif (EzGif.com).
// @namespace      athari
// @author         Athari (https://github.com/Athari)
// @copyright      © Prokhorov ‘Athari’ Alexander, 2024–2025
// @license        MIT
// @homepageURL    https://github.com/Athari/AthariUserJS
// @supportURL     https://github.com/Athari/AthariUserJS/issues
// @version        1.0.0
// @icon           https://www.google.com/s2/favicons?sz=64&domain=ezgif.com
// @match          https://*.ezgif.com/*
// @grant          unsafeWindow
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_getResourceText
// @grant          GM_getResourceURL
// @grant          GM_info
// @run-at         document-start
// @require        https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require        https://cdn.jsdelivr.net/npm/[email protected]/dist/string.min.js
// @require        https://cdn.jsdelivr.net/npm/@athari/[email protected]/monkeyutils.u.min.js
// @resource       script-urlpattern  https://cdn.jsdelivr.net/npm/urlpattern-polyfill/dist/urlpattern.js
// @tag            athari
// ==/UserScript==

(async () => {
  'use strict';

  const convertersUpdatePeriod = 1000 * 60 * 60 * 24 * 7;

  const { waitForDocumentReady, h, u, f, download, attempt, ress, scripts, els, opts } =
    //require("../@athari-monkeyutils/monkeyutils.u"); // TODO
    athari.monkeyutils;

  const res = ress(), script = scripts(res);
  const eld = doc => els(doc, {
    lnkConverters: '#converter-list a',
    ath: {
      selConvFrom: '#ath-conv-from', selConvTo: '#ath-conv-to', btnConvUpdate: '#ath-conv-update',
      lstConverters: "#ath-converters", itmConverter: "#ath-converters li",
    },
  }), el = eld(document);
  const opt = opts({
    converters: [], tools: [], lastConvertersUpdateTime: null, lastConvertersUpdateVersion: null,
  });

  S.extendPrototype();
  Object.assign(globalThis, globalThis.URLPattern ? null : await script.urlpattern);

  await waitForDocumentReady();
  console.log(GM_info);
  const scriptVersionSignature = `${GM_info.script.version}@${new Date(GM_info.script.lastModified ?? 0).toISOString()}`;

  const formatNames = {
    WEBP: "Web Picture (.WEBP)",
    PDF: "Portable Document Format (.PDF)",
    GIF: "Graphics Interchange Format (.GIF)",
    JPG: "JPEG (.JPG .JPEG)",
    JPEG: "JPEG (.JPG .JPEG)",
    APNG: "Animated Portable Network Graphics (.PNG .APNG)",
    JXL: "JPEG XL (.JXL)",
    AVIF: "AV1 Image File (.AVIF)",
    MNG: "Multiple-image Network Graphics (.MNG)",
    MVIMG: "Android JPEG Motion Picture (.MVIMG)",
    ANI: "Animated Windows Cursor (.ANI)",
    HEIC: "HEVC High Efficiency Image File (.HEIC)",
    BMP: "Windows Bitmap (.BMP)",
    BPG: "Better Portable Graphics (.BPG)",
    TGS: "Lottie / Telegram Animated Sticker (.TGS .LOTTIE .JSON)",
    TIFF: "Tagged Image File Format (.TIF .TIFF)",
    HEIF: "High Efficiency Image File (.HEIF)",
    SVG: "Scalable Vector Graphics (.SVG)",
    PNG: "Portable Network Graphics (.PNG)",
    JP2: "JPEG 2000 (.JP2 .J2K .JPM)",
    WEBM: "Web Media (.WEBM)",
    MKV: "Matroska Video (.MKV)",
    MOV: "QuickTime File (.MOV)",
    '3GP': "3GPP File (.3GP)",
    MO: "Compiled GetText Portable Object (.MO)",
    PO: "GetText Portable Object (.PO)",
    CSV: "Comma-Separated Values (.CSV)",
    MP3: "MPEG Audio Layer III (.MP3)",
    MP4: "MPEG-4 Video (.MP4)",
    SPRITE: "Sprite Sheet",
    SPRITES: "Sprite Sheet",
    DATAURI: "Data URI (DATA:)",
  };
  const allTools = {
    Optimize: {
      "GIF": 'optimize',
      "GIF fix": 'repair',
      "PNG": 'optipng',
      "JPEG": 'optijpeg',
      "WEBP": 'optiwebp',
      "Video": 'video-compressor',
    },
    Make: {
      "GIF": 'maker',
      "WEBP": 'webp-maker',
      "APNG": 'apng-maker',
      "AVIF": 'avif-maker',
      "JXL": 'jxl-maker',
      "MNG": 'mng-maker',
    },
    "Extract frames": {
      "GIF": 'split',
      "JPEG": 'video-to-jpg',
      "PNG": 'video-to-png',
      "Sprites": 'sprite-cutter',
    },
    Generate: {
      "QR code": 'qr-generator',
      "Barcode": 'barcode-generator',
    },
    Info: {
      "Metadata": 'view-metadata',
    },
  };

  el.tag.head.insertAdjacentHTML('beforeEnd', /*html*/`
    <style>
      :root {
        color-scheme: light dark;
        --ath-color-background: #fff;
        --ath-color-shadow: #0016;
        --ath-shadow-main: 1px 1px 3px var(--ath-color-shadow);
      }
      @media (prefers-color-scheme: dark) {
        :root {
          --ath-color-background: #212830;
          --ath-color-shadow: #000;
        }
      }
      body {
        display: grid;
        grid-template-areas:
          ". menu main ."
          ". menu foot .";
        grid-template-columns: 1fr 380px auto 1fr;
        min-height: calc(100vh + 1px);
        #wrapper {
          grid-area: main;
          width: auto;
          max-width: 1420px;
          box-shadow: var(--ath-shadow-main);
        }
        footer {
          grid-area: foot;
        }
        .ath-menu {
          box-sizing: border-box;
          grid-area: menu;
          align-self: start;
          position: sticky;
          top: 10px;
          max-height: calc(100vh - 20px);
          margin: 8px;
          padding: 20px;
          display: flex;
          flex-flow: column;
          gap: .7em;
          background: var(--ath-color-background);
          border-radius: 2px;
          box-shadow: var(--ath-shadow-main);
          h3 {
            margin: 0;
          }
        }
      }
      #content {
        #sidebar {
          display: none;
        }
        #main {
          margin: 0;
        }
      }
      #ath-conv-ctls {
        display: flex;
        flex-flow: row;
        gap: .5rem;
        select {
          flex: 1;
          width: 100%;
        }
      }
      .ath-tool-list {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: 0 1em;
        min-height: fit-content;
        margin: 0;
        padding: 0;
        list-style-type: none;
        overflow: hidden auto;
        &.ath-compact {
          grid-template-columns: repeat(4, 1fr);
          gap: 0 .5em;
        }
        li {
          min-width: fit-content;
        }
      }
      #ath-converters {
        height: fit-content;
        min-height: 2lh;
        a {
          display: flex;
          flex-flow: row;
          justify-content: space-between;
          margin-right: 2ch;
          strong {
            opacity: 0.5;
          }
        }
      }
      .ath-hidden {
        display: none;
      }
    </style>`);

  const getFormatName = s => formatNames[s.split(" ")[0].toUpperCase()] ?? s;

  const updateConverters = () => attempt("update converters", async () => {
    const doc = await download("https://ezgif.com/converters", 'html');
    const elDoc = eld(doc);
    const converters = [];
    for (const lnkConv of elDoc.all.lnkConverters) {
      const [ , convFrom, convTo ] = lnkConv.innerText.trim().match(/^(\S+) to (\S+)$/);
      converters.push({
        name: lnkConv.getAttribute('name'),
        title: lnkConv.getAttribute('title'),
        href: lnkConv.getAttribute('href'),
        from: convFrom,
        to: convTo,
      });
    }
    opt.converters = converters;
  });

  const updateControls = () => attempt("update controls", () => {
    const htmlConvertersOptions = (dir) => {
      const formats = _(opt.converters).groupBy(dir).map((cs, format) => ({
        format,
        count: cs.length,
        names: cs.map(c => c.name).join(" "),
      })).value();
      return /*html*/`
        <option value="-">${h(dir)}</option>
        ${formats.map(f => /*html*/`
          <option value="${u(f.format)}" data-converter-names="${h(f.names)}">${h(formatNames[f.format] ?? f.format)} (${h(f.count)})</option>
        `).join("")}
      `;
    };
    el.ath.selConvFrom.innerHTML = htmlConvertersOptions('from');
    el.ath.selConvTo.innerHTML = htmlConvertersOptions('to');
    el.ath.lstConverters.innerHTML = opt.converters.map(c => /*html*/`
      <li data-name="${h(c.name)}" data-from="${h(c.from)}" data-to="${h(c.to)}">
        <a href=${h(c.href)} title="${getFormatName(c.from)} -> ${getFormatName(c.to)}">
          <div>${h(`${c.from}`)}</div>
          <strong>⇒</strong>
          <div>${h(`${c.to}`)}</div>
        </a>
      </li>
    `).join("");
  });

  const expandMenu = () => attempt("expand menu", async () => {
    const updateConvertersList = () => {
      const [ convFrom, convTo ] = [ el.ath.selConvFrom.value, el.ath.selConvTo.value ];
      for (const elConv of el.ath.all.itmConverter)
        elConv.classList.toggle('ath-hidden', !(
          (convFrom === '-' || convFrom === elConv.dataset.from) &&
          (convTo === '-' || convTo === elConv.dataset.to)));
    };
    const updateConvertersAndControls = async () => {
      el.ath.btnConvUpdate.innerText = "Updating...";
      await updateConverters();
      await updateControls();
      el.ath.btnConvUpdate.innerText = "Update";
    };
    el.tag.footer.insertAdjacentHTML('beforeBegin', /*html*/`
      <div class="ath-menu">

        <h3>Convert</h3>
        <div id="ath-conv-ctls">
          <select id="ath-conv-from" title="Convert from">loading</select>
          <select id="ath-conv-to" title="Convert to">loading</select>
          <button id="ath-conv-update">Update</button>
        </div>
        <ul class="ath-tool-list" id="ath-converters">Loading...</ul>

        ${Object.entries(allTools).map(([verb, tools]) => /*html*/`
          <h3>${verb}</h3>
          <ul class="ath-tool-list ath-compact">
            ${Object.entries(tools).map(([title, slug]) => /*html*/`
              <li><a href="/${slug}" title="${getFormatName(title)}">${title}</a></li>
            `).join("")}
          </ul>
        `).join("")}

      </div>`);
    el.ath.btnConvUpdate.onclick = updateConvertersAndControls;
    el.ath.selConvFrom.onchange = updateConvertersList;
    el.ath.selConvTo.onchange = updateConvertersList;
    updateControls();
    updateConvertersList();
  });

  if (opt.lastConvertersUpdateTime == null ||
      opt.lastConvertersUpdateTime + convertersUpdatePeriod > Date.now() ||
      opt.lastConvertersUpdateVersion != scriptVersionSignature) {
    await updateConverters();
    opt.lastConvertersUpdateTime = Date.now();
    opt.lastConvertersUpdateVersion =scriptVersionSignature;
  }
  await expandMenu();
})();