Stackblitz Account Selector (RU)

Интерфейс для сайта stackblitz предоставляющую возможность загружать свою базу аккаунтов (локально) и использовать ее для упрощения логина в разные аккаунты

  1. // ==UserScript==
  2. // @name Stackblitz Account Selector (RU)
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.5
  5. // @description Интерфейс для сайта stackblitz предоставляющую возможность загружать свою базу аккаунтов (локально) и использовать ее для упрощения логина в разные аккаунты
  6. // @author t.me/dud3lk
  7. // @match https://stackblitz.com/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_deleteValue
  11. // @license GNU GPLv3
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17.  
  18. const BACKGROUND_COLOR = '#1D1F24';
  19. const BUTTON_LOGIN_COLOR = '#28a745';
  20. const BUTTON_DELETE_COLOR = '#dc3545';
  21. const BUTTON_UPLOAD_COLOR = '#007bff';
  22.  
  23.  
  24. let logoutObserver = null;
  25. let lastUsedAccount = null;
  26.  
  27.  
  28. if (window.location.href.includes('sign_in')) {
  29. handleLoginPage();
  30. } else {
  31. handleMainPage();
  32. }
  33.  
  34.  
  35. function handleLoginPage() {
  36. waitForLoginFields();
  37. startLogoutDetection();
  38. }
  39.  
  40. function handleMainPage() {
  41. waitForSignInButton();
  42. startLogoutDetection();
  43. }
  44.  
  45.  
  46.  
  47. function startLogoutDetection() {
  48. if (logoutObserver) return;
  49.  
  50. logoutObserver = new MutationObserver((mutations) => {
  51. mutations.forEach((mutation) => {
  52. if (mutation.addedNodes.length) {
  53. const signInLink = document.querySelector('a[href="/sign_in"]');
  54. if (signInLink) {
  55. handleLogoutEvent();
  56. }
  57. }
  58. });
  59. });
  60.  
  61. logoutObserver.observe(document.body, {
  62. childList: true,
  63. subtree: true
  64. });
  65. }
  66.  
  67. function handleLogoutEvent() {
  68. const email = GM_getValue('lastUsedAccount', null);
  69. if (email) {
  70. logLogout(email);
  71. GM_deleteValue('lastUsedAccount');
  72.  
  73.  
  74. const container = document.getElementById('account-selector-container');
  75. if (container) {
  76. container.remove();
  77. showAccountSelector();
  78. }
  79. }
  80. }
  81.  
  82. function logLogout(email) {
  83. const logs = GM_getValue('logoutLogs', {});
  84. logs[email] = new Date().toISOString();
  85. GM_setValue('logoutLogs', logs);
  86. console.log(`[LOGOUT] ${email} at ${logs[email]}`);
  87. }
  88.  
  89.  
  90. function logLogin(email) {
  91. const logs = GM_getValue('loginLogs', {});
  92. logs[email] = new Date().toISOString();
  93. GM_setValue('loginLogs', logs);
  94.  
  95. GM_setValue('lastUsedAccount', email);
  96. console.log(`[LOGIN] ${email} at ${logs[email]}`);
  97. }
  98.  
  99.  
  100. function showAccountSelector() {
  101. const accounts = GM_getValue('accounts', []);
  102. const logoutLogs = GM_getValue('logoutLogs', {});
  103. const loginLogs = GM_getValue('loginLogs', {});
  104.  
  105. if (accounts.length === 0) {
  106. alert('Нет доступных аккаунтов. Загрузите файл.');
  107. return;
  108. }
  109.  
  110. const container = document.createElement('div');
  111. container.id = 'account-selector-container';
  112. container.style.position = 'fixed';
  113. container.style.bottom = '10px';
  114. container.style.right = '10px';
  115. container.style.backgroundColor = BACKGROUND_COLOR;
  116. container.style.padding = '20px';
  117. container.style.border = '1px solid #333';
  118. container.style.zIndex = 9999;
  119. container.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
  120. container.style.borderRadius = '8px';
  121. container.style.width = '450px';
  122. container.style.maxHeight = '600px';
  123. container.style.overflowY = 'auto';
  124. container.style.color = '#fff';
  125.  
  126. const title = document.createElement('h3');
  127. title.textContent = 'Выберите аккаунт:';
  128. title.style.marginBottom = '15px';
  129. title.style.color = '#aaa';
  130. title.style.fontSize = '16px';
  131.  
  132. const list = document.createElement('ul');
  133. list.style.listStyleType = 'none';
  134. list.style.padding = '0';
  135. list.style.margin = '0';
  136.  
  137. accounts.forEach(account => {
  138. const [email] = account.split(':');
  139. const lastLogout = logoutLogs[email];
  140. const lastLogin = loginLogs[email];
  141.  
  142. const listItem = document.createElement('li');
  143. listItem.style.display = 'flex';
  144. listItem.style.justifyContent = 'space-between';
  145. listItem.style.alignItems = 'center';
  146. listItem.style.padding = '10px';
  147. listItem.style.borderBottom = '1px solid #333';
  148. listItem.style.cursor = 'pointer';
  149. listItem.style.transition = 'background-color 0.3s';
  150.  
  151. const emailSpan = document.createElement('span');
  152. emailSpan.style.display = 'flex';
  153. emailSpan.style.flexDirection = 'column';
  154. emailSpan.style.gap = '5px';
  155.  
  156. const emailText = document.createElement('span');
  157. emailText.textContent = email;
  158. emailText.style.color = '#ccc';
  159. emailText.style.fontSize = '14px';
  160.  
  161. const lastLoginSpan = document.createElement('span');
  162. lastLoginSpan.style.color = '#888';
  163. lastLoginSpan.style.fontSize = '12px';
  164. lastLoginSpan.textContent = lastLogin
  165. ? `Вход: ${new Date(lastLogin).toLocaleString()}`
  166. : '';
  167.  
  168. const lastLogoutSpan = document.createElement('span');
  169. lastLogoutSpan.style.color = '#888';
  170. lastLogoutSpan.style.fontSize = '12px';
  171.  
  172. if (lastLogout) {
  173. const logoutDate = new Date(lastLogout);
  174. const now = new Date();
  175. const diffHours = Math.floor((now - logoutDate) / 36e5);
  176.  
  177. lastLogoutSpan.textContent = `Выход: ${logoutDate.toLocaleString()}`;
  178.  
  179. if (diffHours < 24) {
  180. lastLogoutSpan.textContent += ` (Заблокирован на ${24 - diffHours}ч)`;
  181. }
  182. } else {
  183. lastLogoutSpan.textContent = 'Выходов не было';
  184. }
  185.  
  186. emailSpan.appendChild(emailText);
  187. emailSpan.appendChild(lastLoginSpan);
  188. emailSpan.appendChild(lastLogoutSpan);
  189.  
  190. const loginButton = createButton('Войти', BUTTON_LOGIN_COLOR);
  191. loginButton.style.padding = '5px 10px';
  192. loginButton.style.fontSize = '12px';
  193.  
  194. if (lastLogout) {
  195. const logoutDate = new Date(lastLogout);
  196. const now = new Date();
  197. const diffHours = Math.floor((now - logoutDate) / 36e5);
  198.  
  199. if (diffHours < 24) {
  200. loginButton.style.backgroundColor = BUTTON_DELETE_COLOR;
  201. loginButton.title = `Доступно через: ${24 - diffHours} часов`;
  202. }
  203. }
  204.  
  205. loginButton.addEventListener('click', () => {
  206. GM_setValue('selectedAccount', account);
  207. redirectToSignIn();
  208. container.remove();
  209. });
  210.  
  211. listItem.appendChild(emailSpan);
  212. listItem.appendChild(loginButton);
  213. list.appendChild(listItem);
  214. });
  215.  
  216. const deleteButton = createButton('Удалить аккаунты', BUTTON_DELETE_COLOR);
  217. deleteButton.style.marginTop = '15px';
  218. deleteButton.addEventListener('click', () => {
  219. GM_deleteValue('accounts');
  220. GM_deleteValue('logoutLogs');
  221. GM_deleteValue('loginLogs');
  222. alert('Все данные удалены.');
  223. container.remove();
  224. showUploadBasePrompt();
  225. });
  226.  
  227. container.appendChild(title);
  228. container.appendChild(list);
  229. container.appendChild(deleteButton);
  230. document.body.appendChild(container);
  231. }
  232.  
  233.  
  234.  
  235. function createButton(text, color) {
  236. const button = document.createElement('button');
  237. button.textContent = text;
  238. button.style.display = 'inline-block';
  239. button.style.padding = '10px 20px';
  240. button.style.backgroundColor = color;
  241. button.style.color = '#fff';
  242. button.style.border = 'none';
  243. button.style.borderRadius = '4px';
  244. button.style.cursor = 'pointer';
  245. button.style.fontSize = '14px';
  246. button.style.marginTop = '10px';
  247. return button;
  248. }
  249.  
  250. function redirectToSignIn() {
  251. waitForElement('a[href="/sign_in"]', (signInButton) => {
  252. signInButton.click();
  253. const checkForSignInPage = setInterval(() => {
  254. if (window.location.href.includes('sign_in')) {
  255. clearInterval(checkForSignInPage);
  256. waitForLoginFields();
  257. }
  258. }, 200);
  259. setTimeout(() => {
  260. clearInterval(checkForSignInPage);
  261. alert('Переход на страницу входа не выполнен. Попробуйте войти вручную.');
  262. }, 10000);
  263. });
  264. }
  265.  
  266. function waitForLoginFields() {
  267.  
  268. waitForElement('input[name="login"], input[name="password"], button[type="submit"]', () => {
  269. const selectedAccount = GM_getValue('selectedAccount', null);
  270. if (selectedAccount) {
  271. const [email, password] = selectedAccount.split(':');
  272. performLogin(email, password);
  273. } else {
  274. alert('Не выбран аккаунт. Вернитесь на главную страницу и выберите аккаунт.');
  275. }
  276. });
  277. }
  278.  
  279. function performLogin(email, password) {
  280. const emailInput = document.querySelector('input[name="login"]');
  281. const passwordInput = document.querySelector('input[name="password"]');
  282.  
  283. const submitButton = document.querySelector('button[type="submit"]');
  284.  
  285. if (emailInput && passwordInput && submitButton) {
  286. emailInput.value = email;
  287. passwordInput.value = password;
  288. setTimeout(() => {
  289. submitButton.click();
  290. logLogin(email);
  291. GM_deleteValue('selectedAccount');
  292. }, 500);
  293. } else {
  294. alert('Не удалось найти поля для входа. Пожалуйста, войдите вручную.');
  295. }
  296. }
  297.  
  298. function waitForSignInButton() {
  299. waitForElement('a._link_kkm2m_1[href="/sign_in"]', () => {
  300. loadAccounts();
  301. showAccountSelector();
  302. });
  303.  
  304. }
  305.  
  306. function waitForElement(selector, callback) {
  307. const observer = new MutationObserver((mutations) => {
  308. mutations.forEach((mutation) => {
  309. const element = document.querySelector(selector);
  310. if (element) {
  311. observer.disconnect();
  312. callback(element);
  313. }
  314. });
  315. });
  316. observer.observe(document.body, { childList: true, subtree: true });
  317. }
  318.  
  319. function loadAccounts() {
  320. const accounts = GM_getValue('accounts', null);
  321. if (!accounts) {
  322. showUploadBasePrompt();
  323. }
  324. }
  325.  
  326. function showUploadBasePrompt() {
  327. const container = document.createElement('div');
  328. container.style.position = 'fixed';
  329. container.style.bottom = '10px';
  330. container.style.right = '10px';
  331. container.style.backgroundColor = BACKGROUND_COLOR;
  332. container.style.padding = '10px';
  333. container.style.border = '1px solid #333';
  334. container.style.zIndex = 9999;
  335. container.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
  336. container.style.borderRadius = '5px';
  337. container.style.color = '#fff';
  338.  
  339. const label = document.createElement('label');
  340. label.textContent = 'Загрузите базу аккаунтов:';
  341. label.style.display = 'block';
  342. label.style.marginBottom = '10px';
  343. label.style.color = '#aaa';
  344.  
  345. const uploadButton = createButton('Загрузить базу', BUTTON_UPLOAD_COLOR);
  346. uploadButton.addEventListener('click', () => {
  347. createFileInput();
  348. container.remove();
  349. });
  350.  
  351. container.appendChild(label);
  352. container.appendChild(uploadButton);
  353. document.body.appendChild(container);
  354. }
  355.  
  356. function createFileInput() {
  357. const fileInputContainer = document.createElement('div');
  358. fileInputContainer.style.position = 'fixed';
  359. fileInputContainer.style.top = '10px';
  360. fileInputContainer.style.left = '10px';
  361. fileInputContainer.style.backgroundColor = BACKGROUND_COLOR;
  362. fileInputContainer.style.padding = '10px';
  363. fileInputContainer.style.border = '1px solid #333';
  364. fileInputContainer.style.zIndex = 9999;
  365. fileInputContainer.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
  366. fileInputContainer.style.borderRadius = '5px';
  367. fileInputContainer.style.color = '#fff';
  368.  
  369. const label = document.createElement('label');
  370. label.textContent = 'Выберите файл:';
  371. label.style.display = 'block';
  372. label.style.marginBottom = '10px';
  373. label.style.color = '#aaa';
  374.  
  375. const fileInput = document.createElement('input');
  376. fileInput.type = 'file';
  377. fileInput.accept = '.txt';
  378. fileInput.style.display = 'block';
  379.  
  380. fileInput.addEventListener('change', (event) => {
  381. const file = event.target.files[0];
  382. if (file) {
  383. const reader = new FileReader();
  384. reader.onload = (e) => {
  385. const content = e.target.result;
  386. const accountList = content.split('\n').map(line => line.trim()).filter(line => line !== '');
  387. GM_setValue('accounts', accountList);
  388. alert('Аккаунты успешно загружены!');
  389. fileInputContainer.remove();
  390. showAccountSelector();
  391. };
  392. reader.readAsText(file);
  393. }
  394. });
  395.  
  396. fileInputContainer.appendChild(label);
  397. fileInputContainer.appendChild(fileInput);
  398. document.body.appendChild(fileInputContainer);
  399. fileInput.click();
  400. }
  401. })();