// ==UserScript== // @name Installable WebApp Injector with Favicon and Dynamic Theme Color // @namespace http://tampermonkey.net/ // @version 0.4 // @description Makes any website installable as a webapp with dynamic manifest, PWA meta-tags, largest favicon, and automatic theme color detection // @author Kris // @match https://www.nu.nl/* // @grant none // ==/UserScript== (function() { 'use strict'; /** * Get the largest icon from the page or fallback to /favicon.ico * @returns {Promise} URL of the largest icon */ async function getLargestIcon() { const icons = []; // Select all link tags containing "icon" in rel const linkTags = document.querySelectorAll('link[rel*="icon"]'); linkTags.forEach(link => { let size = 0; if (link.sizes && link.sizes.length > 0) { const sizes = Array.from(link.sizes).map(s => parseInt(s.split('x')[0], 10)); size = Math.max(...sizes); } icons.push({ href: link.href, size }); }); // Sort icons descending by size and pick the largest icons.sort((a, b) => b.size - a.size); return icons.length ? icons[0].href : '/favicon.ico'; } /** * Inject or replace the page favicon with the specified icon URL * @param {string} iconURL - URL of the icon to use as favicon */ function injectFavicon(iconURL) { // Remove existing favicon links const existingIcons = document.querySelectorAll('link[rel*="icon"]'); existingIcons.forEach(icon => icon.remove()); // Create new favicon link const faviconLink = document.createElement('link'); faviconLink.rel = 'icon'; faviconLink.href = iconURL; document.head.appendChild(faviconLink); console.log('Favicon injected:', iconURL); } /** * Detect the main theme color of the page * @returns {string} Hex color string */ function detectThemeColor() { const bodyStyle = getComputedStyle(document.body); const bg = bodyStyle.backgroundColor; // If transparent or not set, fallback to white if (!bg || bg === 'transparent' || bg === 'rgba(0, 0, 0, 0)') { return '#ffffff'; } // Convert rgb(a) to hex const rgbMatch = bg.match(/\d+/g); if (rgbMatch && rgbMatch.length >= 3) { const r = parseInt(rgbMatch[0]).toString(16).padStart(2, '0'); const g = parseInt(rgbMatch[1]).toString(16).padStart(2, '0'); const b = parseInt(rgbMatch[2]).toString(16).padStart(2, '0'); return `#${r}${g}${b}`; } return '#ffffff'; } /** * Generate a dynamic manifest and inject it with all necessary PWA meta tags */ async function injectManifest() { const largestIcon = await getLargestIcon(); // Inject favicon injectFavicon(largestIcon); // Get page title const pageTitle = document.querySelector('title')?.innerText || 'WebApp'; // Detect theme color const themeColorValue = detectThemeColor(); // Generate multiple icon resolutions for manifest const iconSizes = [72, 96, 128, 192, 256, 384, 512]; const manifestIcons = iconSizes.map(size => ({ src: largestIcon, sizes: `${size}x${size}`, type: 'image/png' })); // Create the manifest JSON object const manifest = { name: pageTitle, short_name: pageTitle, start_url: '/', display: 'standalone', background_color: themeColorValue, theme_color: themeColorValue, icons: manifestIcons }; // Convert JSON to Blob and generate a URL const blob = new Blob([JSON.stringify(manifest)], { type: 'application/manifest+json' }); const manifestURL = URL.createObjectURL(blob); // Inject into head const link = document.createElement('link'); link.rel = 'manifest'; link.href = manifestURL; document.head.appendChild(link); console.log('Dynamic manifest injected:', manifestURL); // --- Inject Apple-specific meta tags for iOS support --- const appleIcon = document.createElement('link'); appleIcon.rel = 'apple-touch-icon'; appleIcon.href = largestIcon; document.head.appendChild(appleIcon); const appleCapable = document.createElement('meta'); appleCapable.name = 'apple-mobile-web-app-capable'; appleCapable.content = 'yes'; document.head.appendChild(appleCapable); const appleStatus = document.createElement('meta'); appleStatus.name = 'apple-mobile-web-app-status-bar-style'; appleStatus.content = 'black-translucent'; document.head.appendChild(appleStatus); const appleTitle = document.createElement('meta'); appleTitle.name = 'apple-mobile-web-app-title'; appleTitle.content = pageTitle; document.head.appendChild(appleTitle); // --- Inject theme-color meta tag for Android/Chrome --- const themeColor = document.createElement('meta'); themeColor.name = 'theme-color'; themeColor.content = themeColorValue; document.head.appendChild(themeColor); console.log('Theme color detected and applied:', themeColorValue); } // Execute the manifest and favicon injection injectManifest(); })();