RuStore Downloader

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
    }
})();