您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Complete menu with all tools on Ez Gif (EzGif.com).
// ==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(); })();