Search App Store

Search Apple app in your browser

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();