- // ==UserScript==
- // @name Search App Store
- // @namespace https://greasyfork.org/users/136230
- // @version 0.1.0
- // @description Search Apple app in your browser
- // @author eisen-stein
- // @include https://*.apple.com/*
- // @connect apple.com
- // @connect imgur.com
- // @grant GM.xmlHttpRequest
- // ==/UserScript==
-
- /**
- * @typedef {{
- * advisories: string[];
- * appletvScreenshotUrls: string[];
- * artistId: number;
- * artistName: string;
- * artistViewUrl: string;
- * artworkUrl100: string;
- * artworkUrl512: string;
- * artworkUrl60: string;
- * averageUserRating: number;
- * averageUserRatingForCurrentVersion: number;
- * bundleId: string;
- * contentAdvisoryRating: string;
- * currency: string;
- * currentVersionReleaseDate: string;
- * description: string;
- * features: string[];
- * fileSizeBytes: number;
- * formatedPrice: string;
- * genreIds: string[];
- * genres: string[];
- * ipadScreenshotUrls: string[];
- * isGameCenterEnabled: boolean;
- * isVppDeviceBasedLicensingEnabled: boolean;
- * kind: 'software' | 'music' | '';
- * languageCodesISO2A: string[];
- * minimumOsVersion: string;
- * price: number;
- * primaryGenreId: string;
- * primaryGenreName: string;
- * releaseDate: string;
- * releaseNotes: string;
- * screenshotUrls: string[];
- * sellerName: string;
- * sellerUrl: string;
- * supportedDevices: string[];
- * trackCensoredName: string;
- * trackContentRating: string;
- * trackId: number;
- * trackName: string;
- * trackViewUrl: string;
- * userRatingCount: number;
- * userRatingCountForCurrentVersion: number;
- * version: string;
- * wrapperType: 'software' | 'music' | '';
- * }} Software
- */
-
- (function () {
- 'use strict';
-
- DOMReady().then(async () => {
- modalView.create()
- inputView.create()
- await logoView.loadIcon()
- logoView.create()
- })
- async function makeRequest(details) {
- const params = typeof details == 'string' ? { url: details } : Object.assign({}, details)
- let resolve, reject;
- const promise = new Promise((res, rej) => {
- resolve = res;
- reject = rej;
- })
- GM.xmlHttpRequest({
- ...params,
- method: params.method || 'GET',
- url: params.url,
- onload: function (r) {
- const response = {
- data: r.response,
- headers: parseHeaders(r.responseHeaders),
- status: r.status,
- ok: r.status == 200,
- finalUrl: r.finalUrl,
- }
- resolve(response)
- },
- onprogress: function (r) {
- params.onprogress && params.onprogress(r.loaded, r.total);
- },
- onerror: function (r) {
- resolve({
- data: null,
- headers: parseHeaders(r.responseHeaders),
- status: r.status,
- ok: false,
- problem: (r.status || 'error').toString(),
- })
- },
- ontimeout: function (r) {
- resolve({
- data: null,
- headers: parseHeaders(r.responseHeaders),
- status: r.status,
- ok: false,
- problem: 'TIMEOUT',
- })
- },
- onreadystatechange: function (r) {
- },
- })
- return promise;
- }
- function parseHeaders(headersString) {
- if (typeof headersString !== 'string') {
- return headersString
- }
- return headersString.split(/\r?\n/g)
- .map(function (s) { return s.trim() })
- .filter(Boolean)
- .reduce(function (acc, cur) {
- var res = cur.split(':')
- var key, val
- if (res[0]) {
- key = res[0].trim().toLowerCase()
- val = res.slice(1).join('').trim()
- acc[key] = val
- }
- return acc
- }, {})
- }
- /**
- * @param {{
- * responseText: string;
- * headers: { [x: string]: string };
- * ignoreXML?: boolean;
- * responseType?: string;
- * }} params
- */
- function parseResponse(params) {
- var responseText = params.responseText,
- headers = params.headers,
- responseType = params.responseType;
- var isText = !responseType || responseType.toLowerCase() === 'text'
- var contentType = headers['content-type'] || ''
- var ignoreXML = params.ignoreXML === undefined ? true : false;
- if (
- isText
- && contentType.indexOf('application/json') > -1
- ) {
- return JSON.parse(responseText)
- }
- if (
- !ignoreXML
- && isText
- && (
- contentType.indexOf('text/html') > -1
- || contentType.indexOf('text/xml') > -1
- )
- ) {
- return createDocument(responseText)
- }
- return responseText
- }
- function URLEncode(value) {
- return encodeURIComponent(value.replace(/\s+/g, '+'))
- // return value.replace(/\s+/g, '+')
- }
- function URLSearch(params) {
- return Object.keys(params).map(key => {
- return `${key}=${URLEncode(params[key])}`
- }).join('&')
- }
- /**
- * @param {string} packageName
- */
- async function searchAppStore(packageName) {
- const country = navigator.language.slice(0, 2)
- const params = {
- term: packageName,
- country,
- entity: 'software',
- }
- const url = 'https://itunes.apple.com/search?' + URLSearch(params)
- console.log('url = ', url)
- const response = await makeRequest(url)
- /** @type {{ resultCount: number; results: Software[]; }} */
- const data = parseResponse({ responseText: response.data, headers: { 'content-type': 'application/json' } })
- storeView.create(data)
- modalView.show();
- }
- /**
- * @param {Software} data
- */
- function createSoftwareView(data) {
- const view = createView(`
- <div class="app-store-software">
- <image class="app-store-software-icon" src="${data.artworkUrl100}"></image>
- <div class="app-store-software-content">
- <div class="app-store-software-name" >
- <a class="name" href="${data.trackViewUrl}">${data.trackName}</a>
- <span class="version">v${data.version}</span>
- </div>
- <div class="app-store-software-description"><span>${data.description.slice(0, 200)}</span></div>
- <div class="app-store-software-meta">
- <div class="app-store-software-rating">рейтинг: ${data.averageUserRating.toFixed(2)}</div>
- <div class="app-store-software-genres">${data.genres.join(', ')}</div>
- <div class="app-store-software-author">${data.artistName}</div>
- </div>
- </div>
- </div>`
- )
- return view;
- }
- /**
- * @param {string} html
- * @return {HTMLElement}
- */
- function createView(html) {
- const div = document.createElement('div')
- div.innerHTML = (html || '').replace(/\s+/g, ' ').replace(/\r?\n/g, ' ').trim()
- return div.firstElementChild.cloneNode(true)
- }
- function createDocument(html, title) {
- title = title || ''
- var doc = document.implementation.createHTMLDocument(title);
- doc.documentElement.innerHTML = html
- return doc
- }
- function onSubmit(e) {
- e.preventDefault()
- const input = document.querySelector('#package-name')
- if (!input) {
- console.error('input not found')
- return
- }
- const packageName = input.value;
- searchAppStore(packageName).catch((e) => {
- console.error('searchAppStore error: ', e)
- }).then(() => {
- inputView.hide()
- logoView.show()
- });
- }
- function createMainView() {
- const mainView = createView(`<form class="main-view"><div class="input-view-content"></div></form>`)
- const input = createView('<input id="package-name" placeholder="Enter app name" type="text" class="package-name"></input>')
- const submit = createView('<input type="submit" style="display:none"></input>')
- const button = createView('<div class="submit">Submit</div>')
- const div = mainView.querySelector('div')
- div.appendChild(input)
- div.appendChild(submit)
- div.appendChild(button)
-
- button.addEventListener('click', onSubmit)
- mainView.addEventListener('submit', onSubmit);
- return mainView;
- }
- async function DOMReady() {
- if (document.readyState !== 'loading') {
- return
- }
- let resolve
- const promise = new Promise(res => { resolve = res; })
- document.addEventListener('DOMContentLoaded', resolve)
- return promise
- }
- function arrayBufferToBase64(buffer) {
- var binary = '';
- const bytes = new Uint8Array(buffer);
- const len = bytes.byteLength;
- for (let i = 0; i < len; ++i) {
- binary += String.fromCharCode(bytes[i]);
- }
- return window.btoa(binary);
- }
- var modalView = {
- create: function () {
- var element = modalView.getElement()
- if (!element.parentNode) {
- const style = createView(`<style>${modalView.style}</style>`)
- document.head.appendChild(style)
- document.body.appendChild(element)
- }
- return element
- },
- /** @return {HTMLElement} */
- getElement: function () {
- if (modalView.element) {
- return modalView.element
- }
- var element = modalView.element = createView(`
- <div class="modal-wrapper">
- <input type="checkbox" style="display: none; z-index: 1000; position: fixed; top: 10px; left: 10px;" id="modal-checkbox" />
- <div class="modal-container">
- <label for="modal-checkbox" class="modal-close-background" ></label>
- <div class="modal-content">
- ${'' && '<div class="modal-header"><label for="modal-checkbox" title="close" class="modal-close-x"><div></div></label></div>'}
- <div class="modal-body"></div>
- <div class="modal-footer"></div>
- </div>
- </div>
- </div>
- `)
- return element
- },
- /** @param {boolean} checked */
- check: function (checked) {
- var element = modalView.getElement()
- element.querySelector('#modal-checkbox').checked = checked
- },
- show: function () {
- modalView.check(true)
- },
- hide: function () {
- modalView.check(false)
- },
- style: `
- .modal-container {
- position: fixed;
- opacity: 0;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- transition: all 0.25s;
- z-index: -1000;
- }
- #modal-checkbox {
- top: 20px;
- left: 20px;
- position: fixed;
- z-index: 9999999999999;
- display: block;
- }
- #modal-checkbox:checked + .modal-container {
- z-index: 9999999;
- opacity: 1;
- }
- #modal-checkbox:checked + .modal-container label {
- display: block;
- }
- #modal-checkbox:checked + .modal-container .modal-content {
- bottom: 0;
- transition: all 0.25s;
- display: flex;
- }
- .modal-content {
- position: absolute;
- background-color: gray;
- min-width: 400px;
- min-height: 225px;
- max-width: 500px;
- max-height: 280px;
- width: 40%;
- height: 40%;
- opacity: 1;
- flex-direction: column;
- align-items: center;
- right: 0;
- bottom: -20%;
- transition: all 0.25s;
- }
- .modal-header {
- display: flex;
- flex-direction: row;
- position: relative;
- align-items: center;
- width: 100%;
- }
- .modal-close-x {
- margin: 5px 10px 5px 0;
- z-index: 12;
- cursor: pointer;
- }
- .modal-close-x div {
- display: flex;
- flex-direction: row;
- justify-content: center;
- }
- .modal-close-x,
- .modal-close-x div {
- width: 24px;
- height: 24px;
- }
- .modal-close-x div:after,
- .modal-close-x div:before {
- content: "";
- position: absolute;
- background: #fff;
- width: 2.5px;
- height: 24px;
- display: block;
- transform: rotate(45deg);
- }
- .modal-close-x div:before {
- transform: rotate(-45deg);
- }
- .modal-close-background {
- position: absolute;
- background-color: black;
- width: 100%;
- height: 100%;
- opacity: 0.4;
- cursor: pointer;
- display: none;
- }
- `,
- }
- var storeView = {
- create: function (data) {
- var element = storeView.getElement(data)
- if (!element.parentNode) {
- const style = createView(`<style>${storeView.style}</style>`)
- document.head.appendChild(style)
- modalView.getElement().querySelector('.modal-body').appendChild(element)
- }
- return element
- },
- /** @param {{ resultCount: number; results: Software[]; }} data */
- getElement: function (data) {
- if (!storeView.element) {
- storeView.element = createView('<div class="app-store-view"></div>')
- }
- storeView.element.innerHTML = ''
- for (const software of data.results) {
- const sview = createSoftwareView(software)
- storeView.element.appendChild(sview)
- }
- return storeView.element
- },
- style: `
- .modal-content {
- background-color: rgba(255, 255, 255, 0.9);
- }
- .modal-body {
- overflow: auto;
- background-color: rgba(255, 255, 255, 0.9);
- }
- .app-store-view {
- display: flex;
- flex-direction: column;
- }
- .app-store-software {
- display: flex;
- flex-direction: row;
- margin: 5px 0;
- }
- .app-store-software-icon {
- object-fit: contain;
- width: 100px;
- height: 100px;
- }
- .app-store-software-name {
- font-weight: bold;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
- .app-store-software-content {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- margin-left: 10px;
- margin-right: 10px;
- flex: 1;
- }
- .app-store-software-description span {
- text-overflow: ellipsis;
- max-width: 350px;
- white-space: nowrap;
- overflow: hidden;
- display: block;
- }
- .app-store-software-meta {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- }
- .app-store-software-meta > * {
- flex: 1;
- text-align: center;
- }
- `,
- }
- var inputView = {
- create: function () {
- var element = inputView.getElement()
- if (!element.parentNode) {
- const style = createView(`<style>${inputView.style}</style>`)
- document.head.appendChild(style)
- document.body.appendChild(element)
- }
- return element
- },
- getElement: function () {
- if (!inputView.element) {
- inputView.element = createMainView()
- }
- return inputView.element
- },
- hide: function () {
- inputView.getElement().style.display = 'none'
- },
- show: function () {
- inputView.getElement().style.display = 'initial'
- },
- style: `
- .main-view {
- position: fixed;
- top: 60px;
- right: 10px;
- background: #fff;
- padding: 10px;
- z-index: 10000;
- }
- .input-view-content {
- padding: 25px;
- border-radius: 10px;
- border: 1px solid #eaeaea;
- }
- .package-name {
- line-height: 22px;
- font-size: 12px;
- padding-left: 10px;
- }
- .submit {
- color: #fff;
- background-color: #179ed0;
- border-radius: 5px;
- display: flex;
- padding: 3px;
- justify-content: center;
- align-items: center;
- margin-top: 5px;
- }
- `,
- }
-
- var logoView = {
- create: function () {
- var element = logoView.getElement()
- if (!element.parentNode) {
- const style = createView(`<style>${logoView.style}</style>`)
- document.head.appendChild(style)
- document.body.appendChild(element)
- }
- return element
- },
- getElement: function () {
- if (!logoView.element) {
- const logo = logoView.icon// 'https://i.imgur.com/SnBFon3.png'
- logoView.element = createView(`<image src="${logo}" class="app-store-logo" />`)
- logoView.element.addEventListener('click', () => {
- logoView.hide()
- inputView.show()
- })
- }
- return logoView.element
- },
- loadIcon: async function () {
- const response = await makeRequest({
- url: 'https://i.imgur.com/SnBFon3.png',
- responseType: 'arraybuffer',
- })
- if (response.ok) {
- const resource = arrayBufferToBase64(response.data);// URL.createObjectURL(response.data)
- logoView.icon = `data:image/png;base64,${resource}`
- }
- },
- icon: '',
- hide: function () {
- logoView.getElement().style.display = 'none'
- },
- show: function () {
- logoView.getElement().style.display = 'initial'
- },
- style: `
- .app-store-logo {
- position: fixed;
- bottom: 10px;
- right: 10px;
- z-index: 100000;
- width: 60px;
- height: 60px;
- object-fit: contain;
- cursor: pointer;
- }
- `,
- }
-
- })();