RuStore Downloader

Добавление кнопки скачивания APK на страницы приложений RuStore.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         RuStore Downloader
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Добавление кнопки скачивания APK на страницы приложений RuStore.
// @homepageURL  https://gist.github.com/smi-falcon/f75edfb5c0143f81ff6ea25e315f0964
// @supportURL   https://github.com/smi-falcon
// @author       Falcon
// @match        https://www.rustore.ru/catalog/app/*
// @match        https://apps.rustore.ru/app/*
// @icon         https://www.rustore.ru/favicon.ico
// @license      MIT
// @grant        GM_xmlhttpRequest
// @connect      backapi.rustore.ru
// @connect      static.rustore.ru
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        .rustore-download-btn {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 10px 20px;
            background: linear-gradient(135deg, #6B4BFF, #8B6BFF);
            color: white;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(107, 75, 255, 0.3);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        }

        .rustore-download-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(107, 75, 255, 0.5);
            background: linear-gradient(135deg, #8B6BFF, #A78BFF);
        }

        .rustore-download-btn:active {
            transform: translateY(0);
        }

        .rustore-download-btn.loading {
            pointer-events: none;
            opacity: 0.7;
        }

        .rustore-download-btn .spinner {
            display: none;
            width: 16px;
            height: 16px;
            border: 2px solid rgba(255,255,255,0.3);
            border-top: 2px solid white;
            border-radius: 50%;
            animation: rustore-spin 0.8s linear infinite;
        }

        .rustore-download-btn.loading .spinner {
            display: block;
        }

        .rustore-download-btn.loading .btn-text {
            display: none;
        }

        @keyframes rustore-spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .rustore-download-error {
            color: #EF4444;
            font-size: 14px;
            margin-top: 8px;
            padding: 8px 12px;
            background: rgba(239, 68, 68, 0.1);
            border-radius: 8px;
            border: 1px solid rgba(239, 68, 68, 0.2);
        }

        .rustore-split-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 24px;
            border-radius: 16px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            z-index: 10000;
            max-width: 500px;
            width: 90%;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        }

        .rustore-split-modal h3 {
            margin: 0 0 16px 0;
            font-size: 20px;
            font-weight: 600;
            color: #1F2937;
        }

        .rustore-split-modal .close-btn {
            float: right;
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #9CA3AF;
            padding: 0 4px;
        }

        .rustore-split-modal .close-btn:hover {
            color: #4B5563;
        }

        .rustore-split-modal .download-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 12px 16px;
            margin: 8px 0;
            background: #F3F4F6;
            border-radius: 8px;
            cursor: pointer;
            transition: background 0.2s;
        }

        .rustore-split-modal .download-item:hover {
            background: #E5E7EB;
        }

        .rustore-split-modal .download-item .part-name {
            font-weight: 500;
            color: #1F2937;
        }

        .rustore-split-modal .download-item .part-size {
            color: #6B7280;
            font-size: 14px;
        }

        .rustore-split-modal .download-all-btn {
            display: block;
            width: 100%;
            padding: 12px;
            margin-top: 16px;
            background: linear-gradient(135deg, #6B4BFF, #8B6BFF);
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .rustore-split-modal .download-all-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(107, 75, 255, 0.4);
        }

        .rustore-split-modal .install-info {
            background: #FEF3C7;
            padding: 12px;
            border-radius: 8px;
            margin: 12px 0;
            font-size: 14px;
            color: #92400E;
        }

        .rustore-split-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 9999;
        }
    `;
    document.head.appendChild(style);

    function getPackageNameFromUrl() {
        const pathParts = window.location.pathname.split('/').filter(Boolean);
        if (pathParts.length >= 2) {
            return pathParts[pathParts.length - 1];
        }
        return null;
    }

    async function getAppInfo(packageName) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://backapi.rustore.ru/applicationData/overallInfo/${packageName}`,
                headers: {
                    'ruStoreVerCode': '1105002'
                },
                responseType: 'json',
                onload: function(response) {
                    if (response.status === 200 && response.response.code === 'OK') {
                        resolve(response.response.body);
                    } else {
                        reject(new Error('Не удалось получить информацию о приложении'));
                    }
                },
                onerror: function() {
                    reject(new Error('Ошибка сети'));
                }
            });
        });
    }

    async function getDownloadLinkV1(appId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://backapi.rustore.ru/applicationData/download-link',
                headers: {
                    'Content-Type': 'application/json; charset=utf-8',
                    'ruStoreVerCode': '1105002'
                },
                data: JSON.stringify({
                    appId: appId,
                    firstInstall: true
                }),
                responseType: 'json',
                onload: function(response) {
                    if (response.status === 200 && response.response.code === 'OK') {
                        resolve([response.response.body.apkUrl]);
                    } else {
                        reject(new Error('Не удалось получить ссылку на скачивание'));
                    }
                },
                onerror: function() {
                    reject(new Error('Ошибка сети'));
                }
            });
        });
    }

    async function getDownloadLinkV2(appId, sdkVersion) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://backapi.rustore.ru/applicationData/v2/download-link',
                headers: {
                    'Content-Type': 'application/json; charset=utf-8',
                    'ruStoreVerCode': '1105002'
                },
                data: JSON.stringify({
                    appId: appId,
                    firstInstall: true,
                    supportedAbis: ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"],
                    screenDensity: 640,
                    sdkVersion: sdkVersion,
                    withoutSplits: false
                }),
                responseType: 'json',
                onload: function(response) {
                    if (response.status === 200 && response.response.code === 'OK') {
                        const urls = response.response.body.downloadUrls;
                        if (urls && urls.length > 0) {
                            resolve(urls.map(u => u.url));
                        } else {
                            reject(new Error('Не удалось получить URL'));
                        }
                    } else {
                        reject(new Error('Не удалось получить ссылку на скачивание'));
                    }
                },
                onerror: function() {
                    reject(new Error('Ошибка сети'));
                }
            });
        });
    }

    async function getDownloadUrls(appId, sdkVersion) {
        try {
            return await getDownloadLinkV2(appId, sdkVersion);
        } catch (e) {
            return await getDownloadLinkV1(appId);
        }
    }

    function downloadFile(url, filename) {
        const link = document.createElement('a');
        link.href = url;
        link.download = filename || 'app.apk';
        link.style.display = 'none';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    function showSplitModal(urls, baseFilename) {
        const overlay = document.createElement('div');
        overlay.className = 'rustore-split-overlay';
        overlay.id = 'rustore-split-overlay';

        const modal = document.createElement('div');
        modal.className = 'rustore-split-modal';

        let html = `
            <button class="close-btn" id="rustore-modal-close">×</button>
            <h3>Скачать APK</h3>
            <div class="install-info">
                ⚠️ Для установки Split APK используйте приложение SAI: Split APKs Installer или установите все части через ADB.
            </div>
            <div style="max-height: 400px; overflow-y: auto;">
        `;

        urls.forEach((url, index) => {
            const partName = urls.length === 1 ? 'APK' : `Split APK .part${index + 1}`;
            html += `
                <div class="download-item" data-url="${url}" data-index="${index}">
                    <span class="part-name">${partName}</span>
                    <span class="part-size">Нажмите для скачивания</span>
                </div>
            `;
        });

        if (urls.length > 1) {
            html += `
                <button class="download-all-btn" id="rustore-download-all">Скачать все части</button>
            `;
        }

        html += `
            </div>
        `;

        modal.innerHTML = html;
        document.body.appendChild(overlay);
        document.body.appendChild(modal);

        document.getElementById('rustore-modal-close').addEventListener('click', function() {
            overlay.remove();
            modal.remove();
        });

        overlay.addEventListener('click', function() {
            overlay.remove();
            modal.remove();
        });

        document.querySelectorAll('.download-item').forEach(item => {
            item.addEventListener('click', function() {
                const url = this.dataset.url;
                const index = parseInt(this.dataset.index);
                const filename = urls.length === 1
                    ? baseFilename
                    : `${baseFilename.replace('.apk', '')}.part${index + 1}.apk`;
                downloadFile(url, filename);
            });
        });

        const downloadAllBtn = document.getElementById('rustore-download-all');
        if (downloadAllBtn) {
            downloadAllBtn.addEventListener('click', function() {
                urls.forEach((url, index) => {
                    setTimeout(() => {
                        const filename = `${baseFilename.replace('.apk', '')}.Part${index + 1}.apk`;
                        downloadFile(url, filename);
                    }, index * 500);
                });
            });
        }
    }

    function resetButton(button) {
        button.classList.remove('loading');
        button.style.background = 'linear-gradient(135deg, #6B4BFF, #8B6BFF)';
        button.innerHTML = `
            <span class="spinner"></span>
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                Скачать APK
            </span>
        `;
    }

    function showSuccess(button) {
        button.classList.remove('loading');
        button.style.background = 'linear-gradient(135deg, #10B981, #059669)';
        button.innerHTML = `
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <polyline points="20 6 9 17 4 12"></polyline>
                </svg>
                Скачивание началось!
            </span>
        `;
        setTimeout(() => resetButton(button), 5000);
    }

    function showError(button, message) {
        button.classList.remove('loading');
        button.style.background = 'linear-gradient(135deg, #EF4444, #DC2626)';
        button.innerHTML = `
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <circle cx="12" cy="12" r="10"></circle>
                    <line x1="15" y1="9" x2="9" y2="15"></line>
                    <line x1="9" y1="9" x2="15" y2="15"></line>
                </svg>
                Ошибка загрузки
            </span>
        `;

        const existingError = button.parentNode.querySelector('.rustore-download-error');
        if (existingError) {
            existingError.remove();
        }

        const errorDiv = document.createElement('div');
        errorDiv.className = 'rustore-download-error';
        errorDiv.textContent = message;
        button.parentNode.appendChild(errorDiv);

        setTimeout(() => {
            if (errorDiv.parentNode) {
                errorDiv.remove();
            }
        }, 5000);

        setTimeout(() => resetButton(button), 3000);
    }

    function createDownloadButton() {
        const button = document.createElement('button');
        button.className = 'rustore-download-btn';
        button.innerHTML = `
            <span class="spinner"></span>
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                Скачать APK
            </span>
        `;

        button.addEventListener('click', async function(e) {
            e.preventDefault();
            e.stopPropagation();

            button.classList.add('loading');

            try {
                const packageName = getPackageNameFromUrl();
                if (!packageName) {
                    throw new Error('Не удалось определить package name');
                }

                const appInfo = await getAppInfo(packageName);

                if (!appInfo || !appInfo.appId) {
                    throw new Error('Не удалось получить информацию о приложении');
                }

                const urls = await getDownloadUrls(appInfo.appId, appInfo.minSdkVersion);

                if (!urls || urls.length === 0) {
                    throw new Error('Не удалось получить ссылки на скачивание');
                }

                const filename = `${appInfo.packageName}_v${appInfo.versionName}.apk`;

                if (urls.length === 1) {
                    downloadFile(urls[0], filename);
                    showSuccess(button);
                } else {
                    button.classList.remove('loading');
                    showSplitModal(urls, filename);
                    resetButton(button);
                }

            } catch (error) {
                showError(button, error.message);
            }
        });

        return button;
    }

    function insertDownloadButton() {
        const existingButton = document.querySelector('.rustore-download-btn');
        if (existingButton) {
            const container = existingButton.closest('.rustore-download-container');
            if (container) {
                container.remove();
            }
        }

        const targetBlock = document.querySelector('._0oh6ea_1');

        if (!targetBlock) {
            return false;
        }

        let buttonContainer = targetBlock.parentNode.querySelector('.rustore-download-container');
        if (buttonContainer) {
            buttonContainer.remove();
        }

        buttonContainer = document.createElement('div');
        buttonContainer.className = 'rustore-download-container';
        buttonContainer.style.cssText = `
            margin-top: 16px;
            width: 100%;
        `;

        buttonContainer.appendChild(createDownloadButton());
        targetBlock.parentNode.insertBefore(buttonContainer, targetBlock.nextSibling);

        return true;
    }

    function observeUrlChanges() {
        let lastUrl = location.href;

        const domObserver = new MutationObserver(() => {
            if (lastUrl !== location.href) {
                lastUrl = location.href;
                setTimeout(() => {
                    insertDownloadButton();
                }, 500);
            } else {
                if (!document.querySelector('.rustore-download-container')) {
                    insertDownloadButton();
                }
            }
        });

        domObserver.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'style']
        });

        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function() {
            originalPushState.apply(this, arguments);
            setTimeout(() => insertDownloadButton(), 300);
        };

        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            setTimeout(() => insertDownloadButton(), 300);
        };

        window.addEventListener('popstate', () => {
            setTimeout(() => insertDownloadButton(), 300);
        });

        insertDownloadButton();
    }

    function startPeriodicCheck() {
        let checkInterval = setInterval(() => {
            const targetBlock = document.querySelector('._0oh6ea_1');
            const hasButton = document.querySelector('.rustore-download-container');

            if (targetBlock && !hasButton) {
                insertDownloadButton();
            }

            if (window.location.pathname.match(/\/catalog\/app\/|\/app\//) && !hasButton) {
                insertDownloadButton();
            }
        }, 2000);

        setTimeout(() => clearInterval(checkInterval), 30000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            observeUrlChanges();
            startPeriodicCheck();
        });
    } else {
        observeUrlChanges();
        startPeriodicCheck();
    }
})();