Search App Store

Search Apple app in your browser

Устаревшая версия за 20.12.2020. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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;
        }
        `,
    }

})();