EzGif.com – True Menu [Ath]

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

  1. // ==UserScript==
  2. // @name EzGif.com – True Menu [Ath]
  3. // @description Complete menu with all tools on Ez Gif (EzGif.com).
  4. // @namespace athari
  5. // @author Athari (https://github.com/Athari)
  6. // @copyright © Prokhorov ‘Athari’ Alexander, 2024–2025
  7. // @license MIT
  8. // @homepageURL https://github.com/Athari/AthariUserJS
  9. // @supportURL https://github.com/Athari/AthariUserJS/issues
  10. // @version 1.0.0
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=ezgif.com
  12. // @match https://*.ezgif.com/*
  13. // @grant unsafeWindow
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_getResourceText
  17. // @grant GM_getResourceURL
  18. // @grant GM_info
  19. // @run-at document-start
  20. // @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
  21. // @require https://cdn.jsdelivr.net/npm/string@3.3.3/dist/string.min.js
  22. // @require https://cdn.jsdelivr.net/npm/@athari/monkeyutils@0.5.6/monkeyutils.u.min.js
  23. // @resource script-urlpattern https://cdn.jsdelivr.net/npm/urlpattern-polyfill/dist/urlpattern.js
  24. // @tag athari
  25. // ==/UserScript==
  26.  
  27. (async () => {
  28. 'use strict';
  29.  
  30. const convertersUpdatePeriod = 1000 * 60 * 60 * 24 * 7;
  31.  
  32. const { waitForDocumentReady, h, u, f, download, attempt, ress, scripts, els, opts } =
  33. //require("../@athari-monkeyutils/monkeyutils.u"); // TODO
  34. athari.monkeyutils;
  35.  
  36. const res = ress(), script = scripts(res);
  37. const eld = doc => els(doc, {
  38. lnkConverters: '#converter-list a',
  39. ath: {
  40. selConvFrom: '#ath-conv-from', selConvTo: '#ath-conv-to', btnConvUpdate: '#ath-conv-update',
  41. lstConverters: "#ath-converters", itmConverter: "#ath-converters li",
  42. },
  43. }), el = eld(document);
  44. const opt = opts({
  45. converters: [], tools: [], lastConvertersUpdateTime: null, lastConvertersUpdateVersion: null,
  46. });
  47.  
  48. S.extendPrototype();
  49. Object.assign(globalThis, globalThis.URLPattern ? null : await script.urlpattern);
  50.  
  51. await waitForDocumentReady();
  52. console.log(GM_info);
  53. const scriptVersionSignature = `${GM_info.script.version}@${new Date(GM_info.script.lastModified ?? 0).toISOString()}`;
  54.  
  55. const formatNames = {
  56. WEBP: "Web Picture (.WEBP)",
  57. PDF: "Portable Document Format (.PDF)",
  58. GIF: "Graphics Interchange Format (.GIF)",
  59. JPG: "JPEG (.JPG .JPEG)",
  60. JPEG: "JPEG (.JPG .JPEG)",
  61. APNG: "Animated Portable Network Graphics (.PNG .APNG)",
  62. JXL: "JPEG XL (.JXL)",
  63. AVIF: "AV1 Image File (.AVIF)",
  64. MNG: "Multiple-image Network Graphics (.MNG)",
  65. MVIMG: "Android JPEG Motion Picture (.MVIMG)",
  66. ANI: "Animated Windows Cursor (.ANI)",
  67. HEIC: "HEVC High Efficiency Image File (.HEIC)",
  68. BMP: "Windows Bitmap (.BMP)",
  69. BPG: "Better Portable Graphics (.BPG)",
  70. TGS: "Lottie / Telegram Animated Sticker (.TGS .LOTTIE .JSON)",
  71. TIFF: "Tagged Image File Format (.TIF .TIFF)",
  72. HEIF: "High Efficiency Image File (.HEIF)",
  73. SVG: "Scalable Vector Graphics (.SVG)",
  74. PNG: "Portable Network Graphics (.PNG)",
  75. JP2: "JPEG 2000 (.JP2 .J2K .JPM)",
  76. WEBM: "Web Media (.WEBM)",
  77. MKV: "Matroska Video (.MKV)",
  78. MOV: "QuickTime File (.MOV)",
  79. '3GP': "3GPP File (.3GP)",
  80. MO: "Compiled GetText Portable Object (.MO)",
  81. PO: "GetText Portable Object (.PO)",
  82. CSV: "Comma-Separated Values (.CSV)",
  83. MP3: "MPEG Audio Layer III (.MP3)",
  84. MP4: "MPEG-4 Video (.MP4)",
  85. SPRITE: "Sprite Sheet",
  86. SPRITES: "Sprite Sheet",
  87. DATAURI: "Data URI (DATA:)",
  88. };
  89. const allTools = {
  90. Optimize: {
  91. "GIF": 'optimize',
  92. "GIF fix": 'repair',
  93. "PNG": 'optipng',
  94. "JPEG": 'optijpeg',
  95. "WEBP": 'optiwebp',
  96. "Video": 'video-compressor',
  97. },
  98. Make: {
  99. "GIF": 'maker',
  100. "WEBP": 'webp-maker',
  101. "APNG": 'apng-maker',
  102. "AVIF": 'avif-maker',
  103. "JXL": 'jxl-maker',
  104. "MNG": 'mng-maker',
  105. },
  106. "Extract frames": {
  107. "GIF": 'split',
  108. "JPEG": 'video-to-jpg',
  109. "PNG": 'video-to-png',
  110. "Sprites": 'sprite-cutter',
  111. },
  112. Generate: {
  113. "QR code": 'qr-generator',
  114. "Barcode": 'barcode-generator',
  115. },
  116. Info: {
  117. "Metadata": 'view-metadata',
  118. },
  119. };
  120.  
  121. el.tag.head.insertAdjacentHTML('beforeEnd', /*html*/`
  122. <style>
  123. :root {
  124. color-scheme: light dark;
  125. --ath-color-background: #fff;
  126. --ath-color-shadow: #0016;
  127. --ath-shadow-main: 1px 1px 3px var(--ath-color-shadow);
  128. }
  129. @media (prefers-color-scheme: dark) {
  130. :root {
  131. --ath-color-background: #212830;
  132. --ath-color-shadow: #000;
  133. }
  134. }
  135. body {
  136. display: grid;
  137. grid-template-areas:
  138. ". menu main ."
  139. ". menu foot .";
  140. grid-template-columns: 1fr 380px auto 1fr;
  141. min-height: calc(100vh + 1px);
  142. #wrapper {
  143. grid-area: main;
  144. width: auto;
  145. max-width: 1420px;
  146. box-shadow: var(--ath-shadow-main);
  147. }
  148. footer {
  149. grid-area: foot;
  150. }
  151. .ath-menu {
  152. box-sizing: border-box;
  153. grid-area: menu;
  154. align-self: start;
  155. position: sticky;
  156. top: 10px;
  157. max-height: calc(100vh - 20px);
  158. margin: 8px;
  159. padding: 20px;
  160. display: flex;
  161. flex-flow: column;
  162. gap: .7em;
  163. background: var(--ath-color-background);
  164. border-radius: 2px;
  165. box-shadow: var(--ath-shadow-main);
  166. h3 {
  167. margin: 0;
  168. }
  169. }
  170. }
  171. #content {
  172. #sidebar {
  173. display: none;
  174. }
  175. #main {
  176. margin: 0;
  177. }
  178. }
  179. #ath-conv-ctls {
  180. display: flex;
  181. flex-flow: row;
  182. gap: .5rem;
  183. select {
  184. flex: 1;
  185. width: 100%;
  186. }
  187. }
  188. .ath-tool-list {
  189. display: grid;
  190. grid-template-columns: repeat(2, 1fr);
  191. gap: 0 1em;
  192. min-height: fit-content;
  193. margin: 0;
  194. padding: 0;
  195. list-style-type: none;
  196. overflow: hidden auto;
  197. &.ath-compact {
  198. grid-template-columns: repeat(4, 1fr);
  199. gap: 0 .5em;
  200. }
  201. li {
  202. min-width: fit-content;
  203. }
  204. }
  205. #ath-converters {
  206. height: fit-content;
  207. min-height: 2lh;
  208. a {
  209. display: flex;
  210. flex-flow: row;
  211. justify-content: space-between;
  212. margin-right: 2ch;
  213. strong {
  214. opacity: 0.5;
  215. }
  216. }
  217. }
  218. .ath-hidden {
  219. display: none;
  220. }
  221. </style>`);
  222.  
  223. const getFormatName = s => formatNames[s.split(" ")[0].toUpperCase()] ?? s;
  224.  
  225. const updateConverters = () => attempt("update converters", async () => {
  226. const doc = await download("https://ezgif.com/converters", 'html');
  227. const elDoc = eld(doc);
  228. const converters = [];
  229. for (const lnkConv of elDoc.all.lnkConverters) {
  230. const [ , convFrom, convTo ] = lnkConv.innerText.trim().match(/^(\S+) to (\S+)$/);
  231. converters.push({
  232. name: lnkConv.getAttribute('name'),
  233. title: lnkConv.getAttribute('title'),
  234. href: lnkConv.getAttribute('href'),
  235. from: convFrom,
  236. to: convTo,
  237. });
  238. }
  239. opt.converters = converters;
  240. });
  241.  
  242. const updateControls = () => attempt("update controls", () => {
  243. const htmlConvertersOptions = (dir) => {
  244. const formats = _(opt.converters).groupBy(dir).map((cs, format) => ({
  245. format,
  246. count: cs.length,
  247. names: cs.map(c => c.name).join(" "),
  248. })).value();
  249. return /*html*/`
  250. <option value="-">${h(dir)}</option>
  251. ${formats.map(f => /*html*/`
  252. <option value="${u(f.format)}" data-converter-names="${h(f.names)}">${h(formatNames[f.format] ?? f.format)} (${h(f.count)})</option>
  253. `).join("")}
  254. `;
  255. };
  256. el.ath.selConvFrom.innerHTML = htmlConvertersOptions('from');
  257. el.ath.selConvTo.innerHTML = htmlConvertersOptions('to');
  258. el.ath.lstConverters.innerHTML = opt.converters.map(c => /*html*/`
  259. <li data-name="${h(c.name)}" data-from="${h(c.from)}" data-to="${h(c.to)}">
  260. <a href=${h(c.href)} title="${getFormatName(c.from)} -> ${getFormatName(c.to)}">
  261. <div>${h(`${c.from}`)}</div>
  262. <strong>⇒</strong>
  263. <div>${h(`${c.to}`)}</div>
  264. </a>
  265. </li>
  266. `).join("");
  267. });
  268.  
  269. const expandMenu = () => attempt("expand menu", async () => {
  270. const updateConvertersList = () => {
  271. const [ convFrom, convTo ] = [ el.ath.selConvFrom.value, el.ath.selConvTo.value ];
  272. for (const elConv of el.ath.all.itmConverter)
  273. elConv.classList.toggle('ath-hidden', !(
  274. (convFrom === '-' || convFrom === elConv.dataset.from) &&
  275. (convTo === '-' || convTo === elConv.dataset.to)));
  276. };
  277. const updateConvertersAndControls = async () => {
  278. el.ath.btnConvUpdate.innerText = "Updating...";
  279. await updateConverters();
  280. await updateControls();
  281. el.ath.btnConvUpdate.innerText = "Update";
  282. };
  283. el.tag.footer.insertAdjacentHTML('beforeBegin', /*html*/`
  284. <div class="ath-menu">
  285.  
  286. <h3>Convert</h3>
  287. <div id="ath-conv-ctls">
  288. <select id="ath-conv-from" title="Convert from">loading</select>
  289. <select id="ath-conv-to" title="Convert to">loading</select>
  290. <button id="ath-conv-update">Update</button>
  291. </div>
  292. <ul class="ath-tool-list" id="ath-converters">Loading...</ul>
  293.  
  294. ${Object.entries(allTools).map(([verb, tools]) => /*html*/`
  295. <h3>${verb}</h3>
  296. <ul class="ath-tool-list ath-compact">
  297. ${Object.entries(tools).map(([title, slug]) => /*html*/`
  298. <li><a href="/${slug}" title="${getFormatName(title)}">${title}</a></li>
  299. `).join("")}
  300. </ul>
  301. `).join("")}
  302.  
  303. </div>`);
  304. el.ath.btnConvUpdate.onclick = updateConvertersAndControls;
  305. el.ath.selConvFrom.onchange = updateConvertersList;
  306. el.ath.selConvTo.onchange = updateConvertersList;
  307. updateControls();
  308. updateConvertersList();
  309. });
  310.  
  311. if (opt.lastConvertersUpdateTime == null ||
  312. opt.lastConvertersUpdateTime + convertersUpdatePeriod > Date.now() ||
  313. opt.lastConvertersUpdateVersion != scriptVersionSignature) {
  314. await updateConverters();
  315. opt.lastConvertersUpdateTime = Date.now();
  316. opt.lastConvertersUpdateVersion =scriptVersionSignature;
  317. }
  318. await expandMenu();
  319. })();