// ==UserScript==
// @name XCloud Auto Renew Premium
// @namespace http://tampermonkey.net/
// @version 4.3
// @description Tự động gia hạn máy XCloudPhone (v4.3 - Floating Bubble Mode)
// @author Antigravity x Tky567
// @match https://app.xcloudphone.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=xcloudphone.com
// @grant none
// @run-at document-end
// ==/UserScript==
(async function() {
'use strict';
const RENEW_THRESHOLD = 4;
const MAX_LIMIT = 5;
const CHECK_INTERVAL = 5 * 60 * 1000;
const UI_ID = 'xcloud-v4-widget';
let isRefreshing = false;
let nextCheckTime = 0;
let timerInterval = null;
function createUI() {
if (document.getElementById(UI_ID)) return;
const ui = document.createElement('div');
ui.id = UI_ID;
const savedPos = JSON.parse(localStorage.getItem('xcloud_widget_pos') || '{"right":"20px","bottom":"20px"}');
const isMin = localStorage.getItem('xcloud_widget_minimized') === 'true';
// Style cơ bản cho Widget
Object.assign(ui.style, {
position: 'fixed', top: savedPos.top || 'auto', left: savedPos.left || 'auto',
right: (savedPos.top || savedPos.left) ? 'auto' : (savedPos.right || '20px'),
bottom: (savedPos.top || savedPos.left) ? 'auto' : (savedPos.bottom || '20px'),
background: 'rgba(15, 23, 42, 0.95)', color: 'white', zIndex: '2147483647',
fontFamily: 'Inter, system-ui, sans-serif',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5)', border: '1px solid #4f46e5',
backdropFilter: 'blur(10px)', userSelect: 'none', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
overflow: 'hidden', cursor: 'move'
});
// Thiết lập kích thước dựa trên trạng thái thu nhỏ
if (isMin) {
ui.style.width = '48px'; ui.style.height = '48px'; ui.style.borderRadius = '50%'; ui.style.padding = '0';
} else {
ui.style.width = '280px'; ui.style.height = 'auto'; ui.style.borderRadius = '12px'; ui.style.padding = '15px';
}
ui.innerHTML = `
`;
document.body.appendChild(ui);
// --- Logic Chuyển đổi Bubble / Full ---
const bubble = document.getElementById('x-bubble');
const full = document.getElementById('x-full');
const toggleBtn = document.getElementById('x-toggle');
const setMin = (min) => {
if (min) {
ui.style.width = '48px'; ui.style.height = '48px'; ui.style.borderRadius = '50%'; ui.style.padding = '0';
bubble.style.display = 'flex'; full.style.display = 'none';
} else {
ui.style.width = '280px'; ui.style.height = 'auto'; ui.style.borderRadius = '12px'; ui.style.padding = '15px';
bubble.style.display = 'none'; full.style.display = 'block';
}
localStorage.setItem('xcloud_widget_minimized', min);
};
toggleBtn.onclick = (e) => { e.stopPropagation(); setMin(true); };
bubble.onclick = () => { if (!isDraggingMoved) setMin(false); };
// --- Logic Kéo thả ---
let isDragging = false, isDraggingMoved = false, startX, startY, startL, startT;
ui.onmousedown = (e) => {
isDragging = true; isDraggingMoved = false;
startX = e.clientX; startY = e.clientY;
const r = ui.getBoundingClientRect(); startL = r.left; startT = r.top;
ui.style.transition = 'none';
};
document.onmousemove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX; const dy = e.clientY - startY;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) isDraggingMoved = true;
let nX = startL + dx, nY = startT + dy;
nX = Math.max(5, Math.min(nX, window.innerWidth - ui.offsetWidth - 5));
nY = Math.max(5, Math.min(nY, window.innerHeight - ui.offsetHeight - 5));
ui.style.left = nX + 'px'; ui.style.top = nY + 'px'; ui.style.right = ui.style.bottom = 'auto';
};
document.onmouseup = () => {
if (!isDragging) return; isDragging = false;
ui.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
localStorage.setItem('xcloud_widget_pos', JSON.stringify({ left: ui.style.left, top: ui.style.top }));
};
document.getElementById('x-refresh').onclick = (e) => { e.stopPropagation(); runAutoRenew(); };
if (!timerInterval) timerInterval = setInterval(updateCountdown, 1000);
}
function updateCountdown() {
const log = document.getElementById('x-log');
if (!log || nextCheckTime <= 0 || isRefreshing) return;
const remain = Math.max(0, Math.round((nextCheckTime - Date.now()) / 1000));
const m = Math.floor(remain / 60), s = remain % 60;
log.innerText = `v4.3 | Re-check in: ${m}:${s < 10 ? '0' : ''}${s} | by @Tky567`;
if (remain <= 0 && !isRefreshing) runAutoRenew();
}
function updateStatus(type, message) {
const ind = document.getElementById('x-indicator');
const indMin = document.getElementById('x-indicator-min');
const log = document.getElementById('x-log');
const ui = document.getElementById(UI_ID);
if (!log || !ui) return;
log.innerText = message;
const colors = { active: '#22c55e', login: '#f59e0b', error: '#ef4444', neutral: '#94a3b8' };
const color = colors[type] || colors.neutral;
[ind, indMin].forEach(i => { if(i){ i.style.background = color; i.style.boxShadow = `0 0 8px ${color}`; }});
ui.style.borderColor = color;
}
async function runAutoRenew() {
if (isRefreshing) return;
isRefreshing = true;
createUI();
const listEl = document.getElementById('x-list');
try {
const res = await fetch('https://api.xcloudphone.com/renters/rental-sessions?page=1&limit=50', { credentials: 'include' });
if (res.status === 401) {
updateStatus('login', 'Session Expired! Reloading...');
setTimeout(() => location.reload(), 2000); return;
}
const data = await res.json();
const devices = data.data || [];
listEl.innerHTML = '';
const now = new Date(), renewGroups = {};
devices.forEach(d => {
const endTime = new Date(d.endTime);
const diffH = (endTime - now) / 36e5;
const p = Math.min((diffH / MAX_LIMIT) * 100, 100), isU = diffH < 1;
const item = document.createElement('div');
item.style.background = 'rgba(30, 41, 59, 0.5)'; item.style.padding = '6px'; item.style.borderRadius = '6px';
item.innerHTML = `
${d.sessionName}
${Math.floor(diffH)}h ${Math.floor((diffH%1)*60)}m
`;
listEl.appendChild(item);
if (diffH < RENEW_THRESHOLD) {
const hAdd = Math.max(1, Math.floor(MAX_LIMIT - diffH));
if (!renewGroups[hAdd]) renewGroups[hAdd] = [];
renewGroups[hAdd].push(d.id);
}
});
if (Object.keys(renewGroups).length > 0) {
updateStatus('active', 'Renewing...');
for (const [h, ids] of Object.entries(renewGroups)) {
await fetch('https://api.xcloudphone.com/rentals/extend', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listSessionId: ids, rentalHours: Number(h) })
});
}
updateStatus('active', 'Success!');
nextCheckTime = Date.now() + 10000;
} else {
updateStatus('active', 'refresh');
nextCheckTime = Date.now() + CHECK_INTERVAL;
}
} catch (e) {
updateStatus('error', 'Lỗi API');
nextCheckTime = Date.now() + 10000;
}
isRefreshing = false;
}
setTimeout(runAutoRenew, 1000);
})();