RuStore Downloader

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

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

Vous devrez installer une extension telle que Tampermonkey 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         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();
    }
})();