PWAs Anywhere

Allow installing any webpage as a progressive web app

  1. // ==UserScript==
  2. // @name PWAs Anywhere
  3. // @namespace https://www.octt.eu.org/
  4. // @match *://*/*
  5. // @version 1.0.1
  6. // @author OctoSpacc
  7. // @license ISC
  8. // @description Allow installing any webpage as a progressive web app
  9. // @run-at document-idle
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_unregisterMenuCommand
  12. // ==/UserScript==
  13.  
  14. var originalManifest = document.querySelector('link[rel="manifest"]');
  15.  
  16. var menuEntries = {
  17. reinjectDefault : ['📜️ Reinject Default Manifest', reinjectOriginalManifest],
  18. injectCustom : ['📃️ Force Inject Custom Manifest', removeManifestAndInjectCustom],
  19. reinjectCustom : ['📃️ Force Reinject Custom Manifest', removeManifestAndInjectCustom],
  20. removeDefault : ['🚮️ Remove Current Manifest (Default)', removeCurrentManifest],
  21. removeCustom : ['🚮️ Remove Current Manifest (Custom)', removeCurrentManifest],
  22. };
  23.  
  24. function clearMenu () {
  25. for (var entry of Object.values(menuEntries)) {
  26. GM_unregisterMenuCommand(entry[0]);
  27. }
  28. }
  29.  
  30. function menuRegister (key) {
  31. var entry = menuEntries[key];
  32. GM_registerMenuCommand(entry[0], entry[1]);
  33. }
  34.  
  35. function makeManifestElem (href) {
  36. var manifestElem = document.createElement('link');
  37. manifestElem.rel = 'manifest';
  38. manifestElem.href = href;
  39. return manifestElem;
  40. }
  41.  
  42. function removeCurrentManifest () {
  43. var manifestElem = document.querySelector('link[rel="manifest"]');
  44. if (manifestElem) {
  45. manifestElem.parentElement.removeChild(manifestElem);
  46. }
  47. clearMenu();
  48. if (originalManifest) {
  49. menuRegister('reinjectDefault');
  50. }
  51. menuRegister('reinjectCustom');
  52. }
  53.  
  54. function reinjectOriginalManifest () {
  55. document.head.appendChild(makeManifestElem(originalManifest));
  56. clearMenu();
  57. menuRegister('reinjectCustom');
  58. menuRegister('removeDefault');
  59. }
  60.  
  61. function createAndInjectManifest (customIconUrl) {
  62. var descElem = document.querySelector('meta[name="description"]');
  63. var iconElem = (document.querySelector('link[rel~="apple-touch-icon"]') || document.querySelector('link[rel~="icon"]'));
  64. var manifestElem = makeManifestElem('data:application/manifest+json;utf8,' + encodeURIComponent(JSON.stringify({
  65. name: (document.title || location.href),
  66. description: (descElem && descElem.content),
  67. start_url: location.href,
  68. scope: location.href,
  69. display: "standalone",
  70. background_color: getComputedStyle(document.body).backgroundColor,
  71. lang: (document.documentElement.lang || undefined),
  72. icons: [
  73. {
  74. src: (customIconUrl || (iconElem && iconElem.href) || (location.href + '/favicon.ico')),
  75. // type: (iconElem ? (iconElem.type || 'image/png') : 'image/x-icon'),
  76. sizes: "any",
  77. purpose: "any",
  78. },
  79. ],
  80. })));
  81. document.head.appendChild(manifestElem);
  82. menuRegister('reinjectCustom');
  83. menuRegister('removeCustom');
  84. }
  85.  
  86. function removeManifestAndInjectCustom () {
  87. removeCurrentManifest();
  88. createAndInjectManifest(prompt('Optional URL to custom icon (suggested: PNG >= 128x128)? (Will try to get one automatically if unspecified.)'));
  89. }
  90.  
  91. if (originalManifest) {
  92. originalManifest = originalManifest.getAttribute('href');
  93. menuRegister('injectCustom');
  94. menuRegister('removeDefault');
  95. } else {
  96. createAndInjectManifest();
  97. }