Search App Store

Search Apple app in your browser

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Search App Store
// @namespace    https://greatest.deepsurf.us/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;
        }
        `,
    }

})();