// ==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();
})();