Add to Obtainium

Shows an "Add to Obtainium" button on various Obtainium-supported app websites.

  1. // ==UserScript==
  2. // @name Add to Obtainium
  3. // @namespace https://naeembolchhi.github.io/
  4. // @version 0.8
  5. // @description Shows an "Add to Obtainium" button on various Obtainium-supported app websites.
  6. // @author NaeemBolchhi
  7. // @license GPL-3.0-or-later
  8. // @match http*://f-droid.org/*/packages/*/*
  9. // @match http*://cloudflare.f-droid.org/*/packages/*/*
  10. // @match http*://f-droid.org/packages/*/*
  11. // @match http*://cloudflare.f-droid.org/packages/*/*
  12. // @match http*://www.apkmirror.com/apk/*/*
  13. // @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='-464.57' x2='-402.59' y1='485.32' y2='413.87' gradientTransform='matrix(2.83 0 0 -2.83 1398.77 1413.1)' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%239b58dc'/%3E%3Cstop offset='1' stop-color='%23321c92'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M204.93 285.91c-3.79-2.01-25.95-14.29-129.75-71.9-28.88-16.03-56.7-31.45-61.8-34.26C1.78 173.37 0 171.8 0 167.96c0-2.01 4.63-14.89 15.48-43.06 8.51-22.1 16.04-41.47 16.73-43.04 1.57-3.58 4.09-6.61 6.56-7.89 2.31-1.19 60.02-22.27 62.97-23 3.18-.78 5.92.72 7.54 4.14.78 1.64 4 10.1 7.17 18.81 3.17 8.71 7.88 21.58 10.48 28.6 5.95 16.1 6.17 18.38 2.03 20.82-1.1.65-8.23 3.45-15.86 6.22-7.63 2.77-14.9 5.65-16.17 6.39-2.92 1.7-3.36 4.33-1.08 6.44.91.84 15.57 9.2 32.59 18.59 47.56 26.23 44.92 25.04 48.09 21.62 1.55-1.68 29.97-73.94 31.6-80.37.7-2.74.62-3.13-.87-4.33-2.03-1.64-1.74-1.71-20.41 4.89-18.5 6.54-20.26 6.68-22.74 1.79-1.3-2.57-21.23-57.3-22.47-61.7-.8-2.85.18-5.55 2.58-7.13 1-.65 15.5-6.22 32.23-12.36 24.82-9.12 31.14-11.19 34.37-11.24 4.17-.07 4.71.22 81.09 42.48 5.29 2.93 8.12 6.01 8.12 8.86 0 1.13-18.94 51.37-42.09 111.64-32.65 85-42.5 109.94-43.92 111.2-2.43 2.13-4.41 2.04-9.06-.42Z' fill='url(%23a)'/%3E%3C/svg%3E
  14. // @require https://update.greasyfork.org/scripts/535813/1589729/QR-Code-Generator.js
  15. // @run-at document-idle
  16. // @grant none
  17. // ==/UserScript==
  18.  
  19. // SVG Icons
  20. const svgIcons = {
  21. "obtainium": `<svg id="obtainiumlogo" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 300 300"><defs><linearGradient id="a" x1="-464.57" x2="-402.59" y1="485.32" y2="413.87" gradientTransform="matrix(2.83 0 0 -2.83 1398.77 1413.1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9b58dc"/><stop offset="1" stop-color="#321c92"/></linearGradient></defs><path d="M204.93 285.91c-3.79-2.01-25.95-14.29-129.75-71.9-28.88-16.03-56.7-31.45-61.8-34.26C1.78 173.37 0 171.8 0 167.96c0-2.01 4.63-14.89 15.48-43.06 8.51-22.1 16.04-41.47 16.73-43.04 1.57-3.58 4.09-6.61 6.56-7.89 2.31-1.19 60.02-22.27 62.97-23 3.18-.78 5.92.72 7.54 4.14.78 1.64 4 10.1 7.17 18.81 3.17 8.71 7.88 21.58 10.48 28.6 5.95 16.1 6.17 18.38 2.03 20.82-1.1.65-8.23 3.45-15.86 6.22-7.63 2.77-14.9 5.65-16.17 6.39-2.92 1.7-3.36 4.33-1.08 6.44.91.84 15.57 9.2 32.59 18.59 47.56 26.23 44.92 25.04 48.09 21.62 1.55-1.68 29.97-73.94 31.6-80.37.7-2.74.62-3.13-.87-4.33-2.03-1.64-1.74-1.71-20.41 4.89-18.5 6.54-20.26 6.68-22.74 1.79-1.3-2.57-21.23-57.3-22.47-61.7-.8-2.85.18-5.55 2.58-7.13 1-.65 15.5-6.22 32.23-12.36 24.82-9.12 31.14-11.19 34.37-11.24 4.17-.07 4.71.22 81.09 42.48 5.29 2.93 8.12 6.01 8.12 8.86 0 1.13-18.94 51.37-42.09 111.64-32.65 85-42.5 109.94-43.92 111.2-2.43 2.13-4.41 2.04-9.06-.42Z" fill="url(#a)"/></svg>`,
  22. "qr": `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 300 300"><rect width="51.37" height="51.37" x="162.05" y="162.23" rx="25.69" ry="25.69"/><rect width="51.37" height="51.37" x="248.63" y="162.23" rx="25.69" ry="25.69"/><rect width="51.37" height="51.37" x="162.05" y="248.63" rx="25.69" ry="25.69"/><circle cx="231.02" cy="231.11" r="18.68"/><path d="M90.29 0H46.7C20.91 0 0 20.91 0 46.7v43.59c0 25.8 20.91 46.7 46.7 46.7h43.59c25.8 0 46.7-20.91 46.7-46.7V46.7C137 20.91 116.09 0 90.29 0Zm9.34 87.18c0 6.88-5.57 12.45-12.45 12.45H49.82c-6.88 0-12.45-5.57-12.45-12.45V49.82c0-6.88 5.57-12.45 12.45-12.45h37.36c6.88 0 12.45 5.57 12.45 12.45v37.36ZM90.29 163H46.7C20.9 163 0 183.91 0 209.7v43.59c0 25.8 20.91 46.7 46.7 46.7h43.59c25.8 0 46.7-20.91 46.7-46.7V209.7c0-25.8-20.91-46.7-46.7-46.7Zm9.34 87.18c0 6.88-5.57 12.45-12.45 12.45H49.82c-6.88 0-12.45-5.57-12.45-12.45v-37.36c0-6.88 5.57-12.45 12.45-12.45h37.36c6.88 0 12.45 5.57 12.45 12.45v37.36ZM253.3 0h-43.59c-25.8 0-46.7 20.91-46.7 46.7v43.59c0 25.8 20.91 46.7 46.7 46.7h43.59c25.8 0 46.7-20.91 46.7-46.7V46.7C300 20.9 279.09 0 253.3 0Zm9.34 87.18c0 6.88-5.57 12.45-12.45 12.45h-37.36c-6.88 0-12.45-5.57-12.45-12.45V49.82c0-6.88 5.57-12.45 12.45-12.45h37.36c6.88 0 12.45 5.57 12.45 12.45v37.36Z"/></svg>`,
  23. "copy": `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 300 300"><path d="M11.31 114.58C22.19 91.94 42.46 79.36 75 75.16c3.58-23.12 10.99-48.03 35.81-62.02C129.77 2.45 162.65-1.75 208.54.66c30.27 1.59 66.95 8.17 82.09 45.21 6.06 14.84 9.56 40.88 9.36 69.65-.21 29.27-4.33 55.18-11.02 69.32-10.87 22.94-31.21 35.67-63.91 39.91-3.63 23.95-11.4 49.64-38.23 63.25-14.33 7.27-39.9 11.64-70.15 11.97-1.23.01-2.45.02-3.67.02-27.62 0-52.48-3.47-67.01-9.4-21.33-8.7-34.66-24.89-40.74-49.51-7.49-30.29-8.24-96.77 6.05-126.52v.02Zm132.52 147.46c39.08-4.06 43.57-18.9 45.68-57.16 2.39-43.23-1.43-70.68-11.37-81.58-10.53-11.56-37.26-12.79-48.68-13.32-40.48-1.87-69.37 2.44-79.25 11.82-6.22 5.91-9.25 16.62-10.69 24.57-3.17 17.49-2.79 35.85-2.19 51.69.05 1.33.1 2.69.14 4.08.68 20.16 1.52 45.25 20.71 53.95 19.42 8.8 62.82 8.32 85.62 5.95h.03ZM112.54 73.13l24.94.57.26.02c20.04 1.91 37.92 4.28 53.83 13.95 33.83 20.56 34.61 63.71 35.24 98.39 0 .48.02.96.03 1.44 15.71-2.68 26.51-8.32 31.61-24.91 7.06-22.99 6.69-79.29-.67-100.4-4.23-12.14-11.15-18-25.52-21.61-10.93-2.74-28.18-4.21-45.69-4.21-20.89 0-42.15 2.09-53.44 6.62-13.25 5.32-18.04 15.51-20.59 30.15Z"/></svg>`
  24. };
  25.  
  26. // Conditional styles
  27. function styleIf() {
  28. let web = window.location.hostname;
  29.  
  30. if (web.match(/f.droid/)) {
  31. return ``;
  32. } else if (web.match(/apkmirror/)) {
  33. return `
  34. #masthead .site-toolbar .search-bar {
  35. margin-right: 28.5rem;
  36. }
  37. #obtainButton {
  38. right: calc(1.5 * var(--orem));
  39. }
  40. @media screen and (max-width: 991px) {
  41. .searchbox-parent.open {
  42. right: 300px;
  43. }
  44. }
  45. @media screen and (max-width: 750px) {
  46. #obtainButton {
  47. right: calc(1.25 * var(--orem));
  48. }
  49. #masthead .site-toolbar .search-bar {
  50. margin-right: 17.75rem;
  51. }
  52. .searchbox-parent.open {
  53. right: 190px;
  54. }
  55. }
  56. `;
  57. }
  58. }
  59.  
  60. // Button Style
  61. const buttonStyle = `
  62. :root {
  63. --orem: 16px;
  64. }
  65. body .site-title {
  66. width: fit-content;
  67. }
  68. body > .site-wrapper {
  69. position: relative;
  70. }
  71. #obtainButton a {
  72. color: inherit;
  73. text-decoration: none;
  74. opacity: 1;
  75. }
  76. #obtainButton * {
  77. box-sizing: content-box;
  78. transition: transform .05s linear, border-radius .05s linear;
  79. }
  80. #obtainButton {
  81. position: absolute;
  82. right: calc(2 * var(--orem));
  83. margin-top: calc(.75 * var(--orem)) calc(.5 * var(--orem)) calc(.5 * var(--orem)) calc(.5 * var(--orem));
  84. z-index: 999;
  85. -webkit-user-select: none;
  86. -ms-user-select: none;
  87. user-select: none;
  88. display: flex;
  89. justify-content: flex-end;
  90. transition: top .15s linear;
  91. top: calc(-3.5 * var(--orem));
  92. }
  93. #obtainButton.visible {
  94. top: 0;
  95. }
  96. #obtainButton .obtain-fixed {
  97. position: fixed;
  98. display: flex;
  99. align-items: center;
  100. justify-content: center;
  101. }
  102. #obtainButton .obtain-main svg {
  103. height: calc(2 * var(--orem));
  104. width: calc(2 * var(--orem));
  105. }
  106. #obtainButton .obtain-main {
  107. padding: calc(.85 * var(--orem)) calc(.75 * var(--orem)) calc(.5 * var(--orem)) calc(.75 * var(--orem));
  108. display: flex;
  109. align-items: center;
  110. justify-content: center;
  111. gap: calc(.5 * var(--orem));
  112. height: calc(2.25 * var(--orem));
  113. background: #ebe1ff;
  114. transform: translateY(calc(-.35 * var(--orem)));
  115. border-radius: 0 0 0 calc(.5 * var(--orem));
  116. cursor: pointer;
  117. }
  118. #obtainButton .obtain-text {
  119. display: flex;
  120. flex-direction: column;
  121. line-height: 1;
  122. font-family: inherit;
  123. }
  124. #obtainButton .obtain-qr,
  125. #obtainButton .obtain-copy {
  126. padding: calc(.85 * var(--orem)) calc(.5 * var(--orem)) calc(.5 * var(--orem)) calc(.5 * var(--orem));
  127. display: flex;
  128. align-items: center;
  129. justify-content: center;
  130. height: calc(2.25 * var(--orem));
  131. width: calc(2.25 * var(--orem));
  132. transform: translateY(calc(-.35 * var(--orem)));
  133. border-radius: 0 0 calc(.5 * var(--orem)) 0;
  134. cursor: pointer;
  135. background: #9B58DC;
  136. background: linear-gradient(113deg, rgba(155, 88, 220, 1) 0%, rgba(50, 28, 146, 1) 100%);
  137. fill: #ffffff;
  138. }
  139. #obtainButton .obtain-copy {
  140. --_bg-color: #242424;
  141. border-radius: 0 0 0 0;
  142. background: var(--_bg-color);
  143. }
  144. #obtainButton .obtain-main:active,
  145. #obtainButton .obtain-qr:active,
  146. #obtainButton .obtain-copy:active {
  147. transform: translateY(0);
  148. border-radius: 0 0 calc(.5 * var(--orem)) calc(.5 * var(--orem));
  149. }
  150. #obtainButton .obtain-qr svg,
  151. #obtainButton .obtain-copy svg {
  152. height: calc(1.75 * var(--orem));
  153. width: calc(1.75 * var(--orem));
  154. }
  155. #obtainButton .obtain-small {
  156. font-size: calc(.8 * var(--orem));
  157. }
  158. #obtainButton .obtain-large {
  159. font-size: calc(1.2 * var(--orem));
  160. font-weight: 700;
  161. }
  162. #obtainButton #obtain-qr-display {
  163. position: fixed;
  164. top: calc(4 * var(--orem));
  165. border-radius: calc(.5 * var(--orem));
  166. overflow: hidden;
  167. background: #fefefe;
  168. display: none;
  169. height: 0;
  170. max-height: 0;
  171. overflow: hidden;
  172. transition: height .1s ease-in-out, max-height .1s ease-in-out;
  173. box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;
  174. }
  175. #obtainButton #obtain-qr-display canvas {
  176. margin: calc(.75 * var(--orem));
  177. width: calc(40 * var(--orem));
  178. max-width: calc(100svw - (3.5 * var(--orem)));
  179. aspect-ratio: 1/1;
  180. }
  181. #obtainButton #obtain-qr-display img {
  182. display: none;
  183. }
  184. #obtainButton #obtain-qr-display.visible {
  185. display: block;
  186. }
  187. #obtainButton #obtain-qr-display.visible.animate {
  188. height: calc((40 * var(--orem)) + (.75 * var(--orem)) + (.75 * var(--orem)));
  189. max-height: calc(100svw - (3.5 * var(--orem)) + (.75 * var(--orem)) + (.75 * var(--orem)));
  190. }
  191. @media screen and (max-width: 750px) {
  192. #obtainButton {
  193. right: calc(1 * var(--orem));
  194. }
  195. #obtainButton .obtain-fixed .obtain-text {
  196. display: none;
  197. }
  198. }
  199. ` + styleIf();
  200.  
  201. // Generate F-Droid Link
  202. function fdroidLink() {
  203. const appName = document.querySelector('h3.package-name').textContent.replace(/[\t\n]/g,''),
  204. appID = window.location.pathname.replace(/.*packages\/(.*)\//,'$1'),
  205. appURL = window.location.href.replace(/\/$/,'');
  206.  
  207. const settingsJSON = `
  208. {
  209. "filterVersionsByRegEx": "",
  210. "trySelectingSuggestedVersionCode": true,
  211. "autoSelectHighestVersionCode": false,
  212. "trackOnly": false,
  213. "versionExtractionRegEx": "",
  214. "matchGroupToUse": "",
  215. "versionDetection": true,
  216. "useVersionCodeAsOSVersion": false,
  217. "apkFilterRegEx": "",
  218. "invertAPKFilter": false,
  219. "autoApkFilterByArch": true,
  220. "appName": "replacethiswithappnamelater",
  221. "appAuthor": "",
  222. "shizukuPretendToBeGooglePlay": false,
  223. "allowInsecure": false,
  224. "exemptFromBackgroundUpdates": false,
  225. "skipUpdateNotifications": false,
  226. "about": "",
  227. "refreshBeforeDownload": false
  228. }
  229. `.replace(/[\s\n]/g,'').replace(/\"/g,'\\"');
  230.  
  231. const mainJSON = `
  232. {
  233. "id": "${appID}",
  234. "url": "${appURL}",
  235. "author": "replacethiswithauthornamelater",
  236. "name": "replacethiswithappnamelater",
  237. "preferredApkIndex": 0,
  238. "additionalSettings": "replacethiswithsettingslater",
  239. "overrideSource": "FDroid"
  240. }
  241. `.replace(/[\s\n]/g,'')
  242. .replace(/replacethiswithsettingslater/, settingsJSON)
  243. .replace(/replacethiswithauthornamelater/, 'F-Droid official')
  244. .replace(/replacethiswithappnamelater/g, appName);
  245.  
  246. // return `https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/${encodeURIComponent(mainJSON)}`;
  247. return `obtainium://app/${encodeURIComponent(mainJSON)}`;
  248. }
  249.  
  250. // Generate APK Mirror Link
  251. function apkmirrorLink() {
  252. const appAuthor = document.querySelector('.dev-title').textContent.replace(/^by\s/i,''),
  253. appID = document.querySelector('a[href*="play.google.com"]').href.replace(/.*id=/,'') || Date.now(),
  254. urlArray = window.location.pathname.split('/'),
  255. appURL = window.location.origin + `/${urlArray[1]}/${urlArray[2]}/${urlArray[3]}/`,
  256. appName = document.querySelector(`#breadcrumbs a[href='/${urlArray[1]}/${urlArray[2]}/${urlArray[3]}/']`).textContent;
  257.  
  258. const settingsJSON = `
  259. {
  260. "fallbackToOlderReleases": true,
  261. "filterReleaseTitlesByRegEx": "",
  262. "trackOnly": true,
  263. "versionExtractionRegEx": "",
  264. "matchGroupToUse": "",
  265. "versionDetection": true,
  266. "releaseDateAsVersion": false,
  267. "useVersionCodeAsOSVersion": false,
  268. "apkFilterRegEx": "",
  269. "invertAPKFilter": false,
  270. "autoApkFilterByArch": true,
  271. "appName": "replacethiswithappnamelater",
  272. "appAuthor": "replacethiswithauthornamelater",
  273. "shizukuPretendToBeGooglePlay": false,
  274. "allowInsecure": false,
  275. "exemptFromBackgroundUpdates": false,
  276. "skipUpdateNotifications": false,
  277. "about": "",
  278. "refreshBeforeDownload": false
  279. }
  280. `.replace(/[\s\n]/g,'').replace(/\"/g,'\\"');
  281.  
  282. const mainJSON = `
  283. {
  284. "id": "${appID}",
  285. "url": "${appURL}",
  286. "author": "replacethiswithauthornamelater",
  287. "name": "replacethiswithappnamelater",
  288. "preferredApkIndex": 0,
  289. "additionalSettings": "replacethiswithsettingslater",
  290. "overrideSource": null
  291. }
  292. `.replace(/[\s\n]/g,'')
  293. .replace(/replacethiswithsettingslater/, settingsJSON)
  294. .replace(/replacethiswithauthornamelater/g, appAuthor)
  295. .replace(/replacethiswithappnamelater/g, appName);
  296.  
  297. // return `https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/${encodeURIComponent(mainJSON)}`;
  298. return `obtainium://app/${encodeURIComponent(mainJSON)}`;
  299. }
  300.  
  301. // Get right link for Obtainium
  302. function obtainiumLink() {
  303. let web = window.location.hostname;
  304.  
  305. if (web.match(/f.droid/)) {
  306. return fdroidLink();
  307. } else if (web.match(/apkmirror/)) {
  308. return apkmirrorLink();
  309. }
  310. }
  311.  
  312. // Button Content
  313. const buttonContent = `
  314. <div class="obtain-fixed">
  315. <a href="${obtainiumLink()}" class="obtain-main">${svgIcons.obtainium}<div class="obtain-text"><span class="obtain-small">Add to</span><span class="obtain-large">Obtainium</span></div></a>
  316. <div class="obtain-copy">${svgIcons.copy}</div>
  317. <div class="obtain-qr">${svgIcons.qr}</div>
  318. </div>
  319. <div id="obtain-qr-display"></div>
  320. `;
  321.  
  322. // Add a button
  323. function addButton() {
  324. let obtain = document.createElement('obtain');
  325. obtain.innerHTML = `<style type="text/css">${buttonStyle}</style>${buttonContent}`;
  326. obtain.id = "obtainButton";
  327.  
  328. let web = window.location.hostname,
  329. buttonParent = 'body';
  330.  
  331. if (web.match(/f.droid/)) {
  332. buttonParent = 'body > .site-wrapper';
  333. } else if (web.match(/apkmirror/)) {
  334. buttonParent = 'body #masthead .site-toolbar';
  335. }
  336.  
  337. document.querySelector(buttonParent).appendChild(obtain);
  338.  
  339. setTimeout(() => {
  340. document.querySelector('#obtainButton').classList.add('visible');
  341. }, 10);
  342. }
  343.  
  344. // Generate and place QR
  345. function makeQR() {
  346. let qrlink = 'https://apps.obtainium.imranr.dev/redirect?r=' + obtainiumLink();
  347. let target = document.querySelector('#obtain-qr-display');
  348.  
  349. doqr(qrlink, 1, "#fefefe", "#000000", target);
  350. }
  351.  
  352. // Copy function
  353. function copyString(string) {
  354. let copyButton = document.querySelector('.obtain-copy');
  355.  
  356. navigator.clipboard.writeText(string)
  357. .then(() => {
  358. // success
  359. copyButton.setAttribute('style','--_bg-color: #4caf50');
  360. })
  361. .catch(() => {
  362. // failure
  363. copyButton.setAttribute('style','--_bg-color: #f44336');
  364. });
  365.  
  366. setTimeout(() => {
  367. copyButton.removeAttribute('style');
  368. }, 800);
  369. }
  370.  
  371. // Event listeners
  372. function listenClick() {
  373. document.addEventListener('click', (e) => {
  374. let qrButton = document.querySelector('.obtain-qr'),
  375. qrImage = document.querySelector('#obtain-qr-display'),
  376. copyButton = document.querySelector('.obtain-copy')
  377.  
  378. if (qrButton.contains(e.target) && !qrImage.classList.contains('visible')) {
  379. qrImage.classList.add('visible');
  380. setTimeout(() => {
  381. qrImage.classList.add('animate');
  382. }, 10);
  383. } else if (qrButton.contains(e.target) && qrImage.classList.contains('visible')) {
  384. qrImage.classList.remove('animate');
  385. setTimeout(() => {
  386. qrImage.classList.remove('visible');
  387. }, 100);
  388. } else if (!qrImage.contains(e.target) && qrImage.classList.contains('visible')) {
  389. qrImage.classList.remove('animate');
  390. setTimeout(() => {
  391. qrImage.classList.remove('visible');
  392. }, 100);
  393. }
  394.  
  395. if (copyButton.contains(e.target)) {
  396. copyString('https://apps.obtainium.imranr.dev/redirect?r=' + obtainiumLink());
  397. }
  398. });
  399. }
  400.  
  401. addButton();
  402. makeQR();
  403. listenClick();