Добавление кнопки скачивания APK на страницы приложений RuStore.
// ==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();
}
})();