Search App Store

Search Apple app in your browser

  1. // ==UserScript==
  2. // @name Search App Store
  3. // @namespace https://greasyfork.org/users/136230
  4. // @version 0.1.0
  5. // @description Search Apple app in your browser
  6. // @author eisen-stein
  7. // @include https://*.apple.com/*
  8. // @connect apple.com
  9. // @connect imgur.com
  10. // @grant GM.xmlHttpRequest
  11. // ==/UserScript==
  12.  
  13. /**
  14. * @typedef {{
  15. * advisories: string[];
  16. * appletvScreenshotUrls: string[];
  17. * artistId: number;
  18. * artistName: string;
  19. * artistViewUrl: string;
  20. * artworkUrl100: string;
  21. * artworkUrl512: string;
  22. * artworkUrl60: string;
  23. * averageUserRating: number;
  24. * averageUserRatingForCurrentVersion: number;
  25. * bundleId: string;
  26. * contentAdvisoryRating: string;
  27. * currency: string;
  28. * currentVersionReleaseDate: string;
  29. * description: string;
  30. * features: string[];
  31. * fileSizeBytes: number;
  32. * formatedPrice: string;
  33. * genreIds: string[];
  34. * genres: string[];
  35. * ipadScreenshotUrls: string[];
  36. * isGameCenterEnabled: boolean;
  37. * isVppDeviceBasedLicensingEnabled: boolean;
  38. * kind: 'software' | 'music' | '';
  39. * languageCodesISO2A: string[];
  40. * minimumOsVersion: string;
  41. * price: number;
  42. * primaryGenreId: string;
  43. * primaryGenreName: string;
  44. * releaseDate: string;
  45. * releaseNotes: string;
  46. * screenshotUrls: string[];
  47. * sellerName: string;
  48. * sellerUrl: string;
  49. * supportedDevices: string[];
  50. * trackCensoredName: string;
  51. * trackContentRating: string;
  52. * trackId: number;
  53. * trackName: string;
  54. * trackViewUrl: string;
  55. * userRatingCount: number;
  56. * userRatingCountForCurrentVersion: number;
  57. * version: string;
  58. * wrapperType: 'software' | 'music' | '';
  59. * }} Software
  60. */
  61.  
  62. (function () {
  63. 'use strict';
  64.  
  65. DOMReady().then(async () => {
  66. modalView.create()
  67. inputView.create()
  68. await logoView.loadIcon()
  69. logoView.create()
  70. })
  71. async function makeRequest(details) {
  72. const params = typeof details == 'string' ? { url: details } : Object.assign({}, details)
  73. let resolve, reject;
  74. const promise = new Promise((res, rej) => {
  75. resolve = res;
  76. reject = rej;
  77. })
  78. GM.xmlHttpRequest({
  79. ...params,
  80. method: params.method || 'GET',
  81. url: params.url,
  82. onload: function (r) {
  83. const response = {
  84. data: r.response,
  85. headers: parseHeaders(r.responseHeaders),
  86. status: r.status,
  87. ok: r.status == 200,
  88. finalUrl: r.finalUrl,
  89. }
  90. resolve(response)
  91. },
  92. onprogress: function (r) {
  93. params.onprogress && params.onprogress(r.loaded, r.total);
  94. },
  95. onerror: function (r) {
  96. resolve({
  97. data: null,
  98. headers: parseHeaders(r.responseHeaders),
  99. status: r.status,
  100. ok: false,
  101. problem: (r.status || 'error').toString(),
  102. })
  103. },
  104. ontimeout: function (r) {
  105. resolve({
  106. data: null,
  107. headers: parseHeaders(r.responseHeaders),
  108. status: r.status,
  109. ok: false,
  110. problem: 'TIMEOUT',
  111. })
  112. },
  113. onreadystatechange: function (r) {
  114. },
  115. })
  116. return promise;
  117. }
  118. function parseHeaders(headersString) {
  119. if (typeof headersString !== 'string') {
  120. return headersString
  121. }
  122. return headersString.split(/\r?\n/g)
  123. .map(function (s) { return s.trim() })
  124. .filter(Boolean)
  125. .reduce(function (acc, cur) {
  126. var res = cur.split(':')
  127. var key, val
  128. if (res[0]) {
  129. key = res[0].trim().toLowerCase()
  130. val = res.slice(1).join('').trim()
  131. acc[key] = val
  132. }
  133. return acc
  134. }, {})
  135. }
  136. /**
  137. * @param {{
  138. * responseText: string;
  139. * headers: { [x: string]: string };
  140. * ignoreXML?: boolean;
  141. * responseType?: string;
  142. * }} params
  143. */
  144. function parseResponse(params) {
  145. var responseText = params.responseText,
  146. headers = params.headers,
  147. responseType = params.responseType;
  148. var isText = !responseType || responseType.toLowerCase() === 'text'
  149. var contentType = headers['content-type'] || ''
  150. var ignoreXML = params.ignoreXML === undefined ? true : false;
  151. if (
  152. isText
  153. && contentType.indexOf('application/json') > -1
  154. ) {
  155. return JSON.parse(responseText)
  156. }
  157. if (
  158. !ignoreXML
  159. && isText
  160. && (
  161. contentType.indexOf('text/html') > -1
  162. || contentType.indexOf('text/xml') > -1
  163. )
  164. ) {
  165. return createDocument(responseText)
  166. }
  167. return responseText
  168. }
  169. function URLEncode(value) {
  170. return encodeURIComponent(value.replace(/\s+/g, '+'))
  171. // return value.replace(/\s+/g, '+')
  172. }
  173. function URLSearch(params) {
  174. return Object.keys(params).map(key => {
  175. return `${key}=${URLEncode(params[key])}`
  176. }).join('&')
  177. }
  178. /**
  179. * @param {string} packageName
  180. */
  181. async function searchAppStore(packageName) {
  182. const country = navigator.language.slice(0, 2)
  183. const params = {
  184. term: packageName,
  185. country,
  186. entity: 'software',
  187. }
  188. const url = 'https://itunes.apple.com/search?' + URLSearch(params)
  189. console.log('url = ', url)
  190. const response = await makeRequest(url)
  191. /** @type {{ resultCount: number; results: Software[]; }} */
  192. const data = parseResponse({ responseText: response.data, headers: { 'content-type': 'application/json' } })
  193. storeView.create(data)
  194. modalView.show();
  195. }
  196. /**
  197. * @param {Software} data
  198. */
  199. function createSoftwareView(data) {
  200. const view = createView(`
  201. <div class="app-store-software">
  202. <image class="app-store-software-icon" src="${data.artworkUrl100}"></image>
  203. <div class="app-store-software-content">
  204. <div class="app-store-software-name" >
  205. <a class="name" href="${data.trackViewUrl}">${data.trackName}</a>
  206. <span class="version">v${data.version}</span>
  207. </div>
  208. <div class="app-store-software-description"><span>${data.description.slice(0, 200)}</span></div>
  209. <div class="app-store-software-meta">
  210. <div class="app-store-software-rating">рейтинг: ${data.averageUserRating.toFixed(2)}</div>
  211. <div class="app-store-software-genres">${data.genres.join(', ')}</div>
  212. <div class="app-store-software-author">${data.artistName}</div>
  213. </div>
  214. </div>
  215. </div>`
  216. )
  217. return view;
  218. }
  219. /**
  220. * @param {string} html
  221. * @return {HTMLElement}
  222. */
  223. function createView(html) {
  224. const div = document.createElement('div')
  225. div.innerHTML = (html || '').replace(/\s+/g, ' ').replace(/\r?\n/g, ' ').trim()
  226. return div.firstElementChild.cloneNode(true)
  227. }
  228. function createDocument(html, title) {
  229. title = title || ''
  230. var doc = document.implementation.createHTMLDocument(title);
  231. doc.documentElement.innerHTML = html
  232. return doc
  233. }
  234. function onSubmit(e) {
  235. e.preventDefault()
  236. const input = document.querySelector('#package-name')
  237. if (!input) {
  238. console.error('input not found')
  239. return
  240. }
  241. const packageName = input.value;
  242. searchAppStore(packageName).catch((e) => {
  243. console.error('searchAppStore error: ', e)
  244. }).then(() => {
  245. inputView.hide()
  246. logoView.show()
  247. });
  248. }
  249. function createMainView() {
  250. const mainView = createView(`<form class="main-view"><div class="input-view-content"></div></form>`)
  251. const input = createView('<input id="package-name" placeholder="Enter app name" type="text" class="package-name"></input>')
  252. const submit = createView('<input type="submit" style="display:none"></input>')
  253. const button = createView('<div class="submit">Submit</div>')
  254. const div = mainView.querySelector('div')
  255. div.appendChild(input)
  256. div.appendChild(submit)
  257. div.appendChild(button)
  258.  
  259. button.addEventListener('click', onSubmit)
  260. mainView.addEventListener('submit', onSubmit);
  261. return mainView;
  262. }
  263. async function DOMReady() {
  264. if (document.readyState !== 'loading') {
  265. return
  266. }
  267. let resolve
  268. const promise = new Promise(res => { resolve = res; })
  269. document.addEventListener('DOMContentLoaded', resolve)
  270. return promise
  271. }
  272. function arrayBufferToBase64(buffer) {
  273. var binary = '';
  274. const bytes = new Uint8Array(buffer);
  275. const len = bytes.byteLength;
  276. for (let i = 0; i < len; ++i) {
  277. binary += String.fromCharCode(bytes[i]);
  278. }
  279. return window.btoa(binary);
  280. }
  281. var modalView = {
  282. create: function () {
  283. var element = modalView.getElement()
  284. if (!element.parentNode) {
  285. const style = createView(`<style>${modalView.style}</style>`)
  286. document.head.appendChild(style)
  287. document.body.appendChild(element)
  288. }
  289. return element
  290. },
  291. /** @return {HTMLElement} */
  292. getElement: function () {
  293. if (modalView.element) {
  294. return modalView.element
  295. }
  296. var element = modalView.element = createView(`
  297. <div class="modal-wrapper">
  298. <input type="checkbox" style="display: none; z-index: 1000; position: fixed; top: 10px; left: 10px;" id="modal-checkbox" />
  299. <div class="modal-container">
  300. <label for="modal-checkbox" class="modal-close-background" ></label>
  301. <div class="modal-content">
  302. ${'' && '<div class="modal-header"><label for="modal-checkbox" title="close" class="modal-close-x"><div></div></label></div>'}
  303. <div class="modal-body"></div>
  304. <div class="modal-footer"></div>
  305. </div>
  306. </div>
  307. </div>
  308. `)
  309. return element
  310. },
  311. /** @param {boolean} checked */
  312. check: function (checked) {
  313. var element = modalView.getElement()
  314. element.querySelector('#modal-checkbox').checked = checked
  315. },
  316. show: function () {
  317. modalView.check(true)
  318. },
  319. hide: function () {
  320. modalView.check(false)
  321. },
  322. style: `
  323. .modal-container {
  324. position: fixed;
  325. opacity: 0;
  326. top: 0;
  327. right: 0;
  328. bottom: 0;
  329. left: 0;
  330. transition: all 0.25s;
  331. z-index: -1000;
  332. }
  333. #modal-checkbox {
  334. top: 20px;
  335. left: 20px;
  336. position: fixed;
  337. z-index: 9999999999999;
  338. display: block;
  339. }
  340. #modal-checkbox:checked + .modal-container {
  341. z-index: 9999999;
  342. opacity: 1;
  343. }
  344. #modal-checkbox:checked + .modal-container label {
  345. display: block;
  346. }
  347. #modal-checkbox:checked + .modal-container .modal-content {
  348. bottom: 0;
  349. transition: all 0.25s;
  350. display: flex;
  351. }
  352. .modal-content {
  353. position: absolute;
  354. background-color: gray;
  355. min-width: 400px;
  356. min-height: 225px;
  357. max-width: 500px;
  358. max-height: 280px;
  359. width: 40%;
  360. height: 40%;
  361. opacity: 1;
  362. flex-direction: column;
  363. align-items: center;
  364. right: 0;
  365. bottom: -20%;
  366. transition: all 0.25s;
  367. }
  368. .modal-header {
  369. display: flex;
  370. flex-direction: row;
  371. position: relative;
  372. align-items: center;
  373. width: 100%;
  374. }
  375. .modal-close-x {
  376. margin: 5px 10px 5px 0;
  377. z-index: 12;
  378. cursor: pointer;
  379. }
  380. .modal-close-x div {
  381. display: flex;
  382. flex-direction: row;
  383. justify-content: center;
  384. }
  385. .modal-close-x,
  386. .modal-close-x div {
  387. width: 24px;
  388. height: 24px;
  389. }
  390. .modal-close-x div:after,
  391. .modal-close-x div:before {
  392. content: "";
  393. position: absolute;
  394. background: #fff;
  395. width: 2.5px;
  396. height: 24px;
  397. display: block;
  398. transform: rotate(45deg);
  399. }
  400. .modal-close-x div:before {
  401. transform: rotate(-45deg);
  402. }
  403. .modal-close-background {
  404. position: absolute;
  405. background-color: black;
  406. width: 100%;
  407. height: 100%;
  408. opacity: 0.4;
  409. cursor: pointer;
  410. display: none;
  411. }
  412. `,
  413. }
  414. var storeView = {
  415. create: function (data) {
  416. var element = storeView.getElement(data)
  417. if (!element.parentNode) {
  418. const style = createView(`<style>${storeView.style}</style>`)
  419. document.head.appendChild(style)
  420. modalView.getElement().querySelector('.modal-body').appendChild(element)
  421. }
  422. return element
  423. },
  424. /** @param {{ resultCount: number; results: Software[]; }} data */
  425. getElement: function (data) {
  426. if (!storeView.element) {
  427. storeView.element = createView('<div class="app-store-view"></div>')
  428. }
  429. storeView.element.innerHTML = ''
  430. for (const software of data.results) {
  431. const sview = createSoftwareView(software)
  432. storeView.element.appendChild(sview)
  433. }
  434. return storeView.element
  435. },
  436. style: `
  437. .modal-content {
  438. background-color: rgba(255, 255, 255, 0.9);
  439. }
  440. .modal-body {
  441. overflow: auto;
  442. background-color: rgba(255, 255, 255, 0.9);
  443. }
  444. .app-store-view {
  445. display: flex;
  446. flex-direction: column;
  447. }
  448. .app-store-software {
  449. display: flex;
  450. flex-direction: row;
  451. margin: 5px 0;
  452. }
  453. .app-store-software-icon {
  454. object-fit: contain;
  455. width: 100px;
  456. height: 100px;
  457. }
  458. .app-store-software-name {
  459. font-weight: bold;
  460. display: flex;
  461. flex-direction: row;
  462. justify-content: space-between;
  463. }
  464. .app-store-software-content {
  465. display: flex;
  466. flex-direction: column;
  467. justify-content: space-between;
  468. margin-left: 10px;
  469. margin-right: 10px;
  470. flex: 1;
  471. }
  472. .app-store-software-description span {
  473. text-overflow: ellipsis;
  474. max-width: 350px;
  475. white-space: nowrap;
  476. overflow: hidden;
  477. display: block;
  478. }
  479. .app-store-software-meta {
  480. display: flex;
  481. flex-direction: row;
  482. justify-content: space-between;
  483. align-items: center;
  484. }
  485. .app-store-software-meta > * {
  486. flex: 1;
  487. text-align: center;
  488. }
  489. `,
  490. }
  491. var inputView = {
  492. create: function () {
  493. var element = inputView.getElement()
  494. if (!element.parentNode) {
  495. const style = createView(`<style>${inputView.style}</style>`)
  496. document.head.appendChild(style)
  497. document.body.appendChild(element)
  498. }
  499. return element
  500. },
  501. getElement: function () {
  502. if (!inputView.element) {
  503. inputView.element = createMainView()
  504. }
  505. return inputView.element
  506. },
  507. hide: function () {
  508. inputView.getElement().style.display = 'none'
  509. },
  510. show: function () {
  511. inputView.getElement().style.display = 'initial'
  512. },
  513. style: `
  514. .main-view {
  515. position: fixed;
  516. top: 60px;
  517. right: 10px;
  518. background: #fff;
  519. padding: 10px;
  520. z-index: 10000;
  521. }
  522. .input-view-content {
  523. padding: 25px;
  524. border-radius: 10px;
  525. border: 1px solid #eaeaea;
  526. }
  527. .package-name {
  528. line-height: 22px;
  529. font-size: 12px;
  530. padding-left: 10px;
  531. }
  532. .submit {
  533. color: #fff;
  534. background-color: #179ed0;
  535. border-radius: 5px;
  536. display: flex;
  537. padding: 3px;
  538. justify-content: center;
  539. align-items: center;
  540. margin-top: 5px;
  541. }
  542. `,
  543. }
  544.  
  545. var logoView = {
  546. create: function () {
  547. var element = logoView.getElement()
  548. if (!element.parentNode) {
  549. const style = createView(`<style>${logoView.style}</style>`)
  550. document.head.appendChild(style)
  551. document.body.appendChild(element)
  552. }
  553. return element
  554. },
  555. getElement: function () {
  556. if (!logoView.element) {
  557. const logo = logoView.icon// 'https://i.imgur.com/SnBFon3.png'
  558. logoView.element = createView(`<image src="${logo}" class="app-store-logo" />`)
  559. logoView.element.addEventListener('click', () => {
  560. logoView.hide()
  561. inputView.show()
  562. })
  563. }
  564. return logoView.element
  565. },
  566. loadIcon: async function () {
  567. const response = await makeRequest({
  568. url: 'https://i.imgur.com/SnBFon3.png',
  569. responseType: 'arraybuffer',
  570. })
  571. if (response.ok) {
  572. const resource = arrayBufferToBase64(response.data);// URL.createObjectURL(response.data)
  573. logoView.icon = `data:image/png;base64,${resource}`
  574. }
  575. },
  576. icon: '',
  577. hide: function () {
  578. logoView.getElement().style.display = 'none'
  579. },
  580. show: function () {
  581. logoView.getElement().style.display = 'initial'
  582. },
  583. style: `
  584. .app-store-logo {
  585. position: fixed;
  586. bottom: 10px;
  587. right: 10px;
  588. z-index: 100000;
  589. width: 60px;
  590. height: 60px;
  591. object-fit: contain;
  592. cursor: pointer;
  593. }
  594. `,
  595. }
  596.  
  597. })();