您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Generate or repair a Web Manifest for any web page
当前为
// ==UserScript== // @name Installability: Every web page is an installable app! // @description Generate or repair a Web Manifest for any web page // @namespace Itsnotlupus Industries // @match https://*/* // @grant none // @version 1.0 // @noframes // @author itsnotlupus // @license MIT // @require https://gf.qytechs.cn/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js // @grant GM_xmlhttpRequest // @grant GM_addElement // @grant GM_getValue // @grant GM_setValue // ==/UserScript== /*jshint esversion:11 */ /** * A simple premise: Make every web page installable as an app on your local device. * * WHY? * * Your "app" will be present in your list of apps. * It will load without the usual browser chrome, and if you squint the right way, you might come to think of it as an app rather than a web page. * * HOW? * * By generating a Web Manifest automatically, using what scraps of data we can find hidden within the page. * * THAT.. THAT SOUNDS IFFY. * * That's not a question. And also, yes. * If we can't find a big enough app icon, we'll upscale whatever we find. It might be ugly. * If the web page has security rules preventing us from injecting our own manifest in the page, this won't work. * * Also, browsers may not be in a rush to notice we set/updated the page's manifest. patience. * */ const CACHE_MANIFEST_EXPIRATION = 24*3600*1000; // keep generated manifests on any given site for 24 hours before generating new ones. const resolveURI = (uri, base=location.href) => uri && new URL(uri, base).toString(); /** * load an image without CSP restrictions. */ function getImage(src) {1 return new Promise((resolve) => { const img = GM_addElement('img', { src: resolveURI(src), crossOrigin: "anonymous" }); img.onload = () => resolve(img); img.onerror = () => resolve(null); img.remove(); }); } function grabURL(src) { return new Promise(resolve => { const url = resolveURI(src); GM_xmlhttpRequest({ url, responseType: 'blob', anonymous: true, async onload(res) { resolve(res.response); }, onerror() { console.error("Couldn't grab URL " + s); resolve(null); } }); }); } /** * Grab an image and its mime-type regardless of browser sandbox limitations. */ async function getUntaintedImage(src) { const blob = await grabURL(src); const blobURL = URL.createObjectURL(blob); const img = await getImage(blobURL); if (!img) return null; // the URL returned a non-image. URL.revokeObjectURL(blobURL); return { src: resolveURI(src), img, width: img.naturalWidth, height: img.naturalHeight, type: blob.type }; } function makeBigPNG(fromImg) { // scale to at least 144x144, but keep the pixels if there are more. const width = Math.max(144, fromImg.width); const height = Math.max(144, fromImg.height); const canvas = crel('canvas', { width, height }); const ctx = canvas.getContext('2d'); ctx.drawImage(fromImg, 0, 0, width, height); const url = canvas.toDataURL({ type: "image/png" }); return { src: url, width, height, type: "image/png" }; } async function repairManifest() { console.log("Manifest found. Analyzing for problems.."); let fixed = false; const manifestURL = $`link[rel="manifest"]`.href; const manifestBlob = await grabURL(manifestURL); const manifest = JSON.parse(await manifestBlob.text()); // fix manifests with missing start_url if (!manifest.start_url) { manifest.start_url = location.origin; fixed = true; } if (!["standalone", "fullscreen", "minimal-ui"].includes(manifest.display)) { manifest.display = "minimal-ui"; fixed = true; } if (fixed) { // since we're loading the manifest from a data: URI, fix all the relative URIs manifest.icons.forEach(img => img.src= resolveURI(img.src, manifestURL)); ["start_url", "scope"].forEach(k => manifest[k] = resolveURI(manifest[k], manifestURL)); $`link[rel="manifest"]`.remove(); return manifest; } // nothing to do, let the original manifest stand.nothing. console.log("Manifest seems valid. Good to go."); return null; } async function generateManifest() { // Remember how there's this universal way to get a web site's name? Yeah, me neither. const goodNames = [ // plausible places to find one $`meta[name="application-name"]`?.content, $`meta[name="apple-mobile-web-app-title"]`?.content, $`meta[name="al:android:app_name"]`?.content, $`meta[name="al:ios:app_name"]`?.content, $`meta[property="og:site_name"]`?.content, $`meta[property="og:title"]`?.content, ].filter(v=>!!v).sort((a,b)=>a.length-b.length); // short names first. const badNames = [ // various bad ideas $`link[rel="search]"`?.title.replace(/ search/i,''), document.title, $`h1`?.textContent, [...location.hostname.replace(/^www\./,'')].map((c,i)=>i?c:c.toUpperCase()).join('') // capitalized domain name. If everything else fails, there's at least this. ].filter(v=>!!v); const short_name = goodNames[0] ?? badNames[0]; const app_name = goodNames[-1] ?? badNames[0]; const descriptions = [ $`meta[property="og:description"]`?.content, $`meta[name="description"]`?.content, $`meta[name="description"]`?.getAttribute("value"), $`meta[name="twitter:description"]`?.content, ].filter(v=>!!v); const app_description = descriptions[0]; const icons = [ ...Array.from($$`link[rel*="icon"]`).filter(link=>link.rel!="mask-icon").map(link=>link.href), resolveURI($`meta[itemprop="image"]`?.content), "/favicon.ico", // well known location for tiny site icons. ].filter(v=>!!v); // fetch all the icons, so we know what we're working with. const images = (await Promise.all(icons.map(getUntaintedImage))).filter(v=>!!v); images.sort((a,b)=>b.height - a.height); // largest image first. if (!images.length) { console.error("Could not find any app icon here. Giving up on creating a manifest.") return; // just give up. we can't install an app without an icon. } // grab the biggest one. const biggestImage = images[0]; if (biggestImage.width < 144 || biggestImage.height < 144 || biggestImage.type !== 'image/png') { console.log(`We may not have a valid icon yet, scaling an image of type ${biggestImage.type} and size (${biggestImage.width}x${biggestImage.height}) into a big enough PNG.`); // welp, we're gonna scale it. const img = await makeBigPNG(biggestImage.img); images.unshift(img); } const colors = [ $`meta[name="theme-color"]`?.content, getComputedStyle(document.body).backgroundColor ].filter(v=>!!v); const theme_color = colors[0]; const background_color = colors.at(-1); // There it is, our glorious Web Manifest. return { name: app_name, short_name: short_name, description: app_description, start_url: location.href, display: "standalone", theme_color: theme_color, background_color: background_color, icons: images.map(img => ({ src: img.src, sizes: `${img.width}x${img.height}`, type: img.type })) }; } async function main() { const start = Date.now(); let manifest; const cached_item = GM_getValue("manifest:"+location.origin); if (cached_item && cached_item.expires > Date.now()) { // shortcut everything manifest = cached_item.manifest; manifest.start_url = location.href; // don't forget to blow up any pre-existing manifest $`link[rel="manifest"]`?.remove(); } else { if ($`link[rel="manifest"]`) { manifest = await repairManifest(); } else { manifest = await generateManifest(); } if (manifest) { GM_setValue("manifest:"+location.origin, { expires: Date.now() + CACHE_MANIFEST_EXPIRATION, manifest }); console.log("New manifest generated.", JSON.stringify(manifest,null,2).replace(/"data:.*?"/g,`"data: URI removed"`)); } } if (manifest) { // Use GM_addElement to inject the manifest. // It doesn't succeed in bypassing Content Security Policy rules today, but maybe userscript extensions will make this work someday. GM_addElement(document.head, 'link', { rel: "manifest", href: 'data:application/manifest+json,'+encodeURIComponent(JSON.stringify(manifest)) }); } } main();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址