RuStore Downloader

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         RuStore Downloader
// @namespace    http://tampermonkey.net/
// @version      1.2
// @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);
        }
    `;
    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': '247'
                },
                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 getDownloadLink(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',
                    'ruStoreVerCode': '247'
                },
                data: JSON.stringify({
                    appId: appId,
                    firstInstall: true,
                    mobileServices: [],
                    supportedAbis: ["x86_64", "arm64-v8a", "x86", "armeabi-v7a", "armeabi"],
                    screenDensity: 0,
                    supportedLocales: ["ru_RU"],
                    sdkVersion: sdkVersion,
                    withoutSplits: true,
                    signatureFingerprint: null
                }),
                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('Ошибка сети'));
                }
            });
        });
    }

    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 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), 3000);
    }

    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 downloadInfo = await getDownloadLink(appInfo.appId, appInfo.minSdkVersion);

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

                const downloadUrl = downloadInfo.downloadUrls[0].url;
                const filename = `${appInfo.packageName}_v${appInfo.versionName}.apk`;

                downloadFile(downloadUrl, filename);
                showSuccess(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();
    }
})();