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