Автоматическое выделение и удаление площадок по заданным маскам
Size
16.5 KB
Version
1.2.5
Created
Mar 25, 2026
Updated
22 days ago
1// ==UserScript==
2// @name Yandex Direct - Удаление площадок по маскам
3// @description Автоматическое выделение и удаление площадок по заданным маскам
4// @version 1.2.5
5// @match https://*.direct.yandex.ru/*
6// @icon https://direct.yastatic.net/s3/direct-frontend/uac/desktop/assets/b7b733df183b603b.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Yandex Direct - Удаление площадок по маскам: расширение запущено');
12
13 // Дебаунс функция для оптимизации
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Функция для создания модального окна
27 function createModal() {
28 const modal = document.createElement('div');
29 modal.id = 'platform-remover-modal';
30 modal.style.cssText = `
31 position: fixed;
32 top: 0;
33 left: 0;
34 width: 100%;
35 height: 100%;
36 background: rgba(0, 0, 0, 0.5);
37 display: flex;
38 justify-content: center;
39 align-items: center;
40 z-index: 10000;
41 `;
42
43 const modalContent = document.createElement('div');
44 modalContent.style.cssText = `
45 background: white;
46 padding: 30px;
47 border-radius: 12px;
48 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
49 max-width: 500px;
50 width: 90%;
51 `;
52
53 const title = document.createElement('h2');
54 title.textContent = 'Удаление площадок по маскам';
55 title.style.cssText = `
56 margin: 0 0 20px 0;
57 font-size: 20px;
58 color: #333;
59 `;
60
61 const description = document.createElement('p');
62 description.textContent = 'Введите маски для удаления (каждая с новой строки):';
63 description.style.cssText = `
64 margin: 0 0 10px 0;
65 font-size: 14px;
66 color: #666;
67 `;
68
69 const textarea = document.createElement('textarea');
70 textarea.id = 'platform-masks-input';
71 textarea.value = 'dsp\ncom.\ngame\nfree\nvpn';
72 textarea.style.cssText = `
73 width: 100%;
74 height: 150px;
75 padding: 10px;
76 border: 1px solid #ddd;
77 border-radius: 6px;
78 font-size: 14px;
79 font-family: monospace;
80 resize: vertical;
81 box-sizing: border-box;
82 `;
83
84 const statusDiv = document.createElement('div');
85 statusDiv.id = 'platform-remover-status';
86 statusDiv.style.cssText = `
87 margin: 15px 0;
88 padding: 10px;
89 border-radius: 6px;
90 font-size: 14px;
91 display: none;
92 `;
93
94 const buttonsDiv = document.createElement('div');
95 buttonsDiv.style.cssText = `
96 display: flex;
97 gap: 10px;
98 margin-top: 20px;
99 `;
100
101 const startButton = document.createElement('button');
102 startButton.textContent = 'Запустить';
103 startButton.style.cssText = `
104 flex: 1;
105 padding: 12px 24px;
106 background: #fc0;
107 color: #000;
108 border: none;
109 border-radius: 6px;
110 font-size: 14px;
111 font-weight: 600;
112 cursor: pointer;
113 transition: background 0.2s;
114 `;
115 startButton.onmouseover = () => startButton.style.background = '#ffdb4d';
116 startButton.onmouseout = () => startButton.style.background = '#fc0';
117
118 const stopButton = document.createElement('button');
119 stopButton.textContent = 'Стоп';
120 stopButton.style.cssText = `
121 flex: 1;
122 padding: 12px 24px;
123 background: #ff4444;
124 color: #fff;
125 border: none;
126 border-radius: 6px;
127 font-size: 14px;
128 font-weight: 600;
129 cursor: pointer;
130 transition: background 0.2s;
131 display: none;
132 `;
133 stopButton.onmouseover = () => stopButton.style.background = '#ff6666';
134 stopButton.onmouseout = () => stopButton.style.background = '#ff4444';
135
136 const cancelButton = document.createElement('button');
137 cancelButton.textContent = 'Отмена';
138 cancelButton.style.cssText = `
139 flex: 1;
140 padding: 12px 24px;
141 background: #f0f0f0;
142 color: #333;
143 border: none;
144 border-radius: 6px;
145 font-size: 14px;
146 font-weight: 600;
147 cursor: pointer;
148 transition: background 0.2s;
149 `;
150 cancelButton.onmouseover = () => cancelButton.style.background = '#e0e0e0';
151 cancelButton.onmouseout = () => cancelButton.style.background = '#f0f0f0';
152
153 startButton.onclick = () => startProcessing(textarea.value, statusDiv, startButton, stopButton, cancelButton);
154 cancelButton.onclick = () => modal.remove();
155
156 buttonsDiv.appendChild(startButton);
157 buttonsDiv.appendChild(stopButton);
158 buttonsDiv.appendChild(cancelButton);
159
160 modalContent.appendChild(title);
161 modalContent.appendChild(description);
162 modalContent.appendChild(textarea);
163 modalContent.appendChild(statusDiv);
164 modalContent.appendChild(buttonsDiv);
165
166 modal.appendChild(modalContent);
167 document.body.appendChild(modal);
168
169 console.log('Модальное окно создано');
170 }
171
172 // Функция для обновления статуса
173 function updateStatus(statusDiv, message, type = 'info') {
174 statusDiv.style.display = 'block';
175 statusDiv.textContent = message;
176
177 if (type === 'success') {
178 statusDiv.style.background = '#d4edda';
179 statusDiv.style.color = '#155724';
180 statusDiv.style.border = '1px solid #c3e6cb';
181 } else if (type === 'error') {
182 statusDiv.style.background = '#f8d7da';
183 statusDiv.style.color = '#721c24';
184 statusDiv.style.border = '1px solid #f5c6cb';
185 } else {
186 statusDiv.style.background = '#d1ecf1';
187 statusDiv.style.color = '#0c5460';
188 statusDiv.style.border = '1px solid #bee5eb';
189 }
190
191 console.log(`Статус: ${message}`);
192 }
193
194 // Функция для получения всех строк таблицы
195 function getTableRows() {
196 return document.querySelectorAll('[data-testid^="Grid.Row"]');
197 }
198
199 // Функция для получения названия площадки из строки
200 function getPlatformName(row) {
201 const cells = row.querySelectorAll('[data-testid^="Grid.Cell"]');
202 if (cells.length > 1) {
203 return cells[1].textContent.trim();
204 }
205 return '';
206 }
207
208 // Функция для клика по чекбоксу в строке
209 function clickCheckbox(row) {
210 // Сначала проверяем, не отмечен ли уже чекбокс
211 const checkedBox = row.querySelector('[data-testid*="Checkbox"][data-checked="true"]');
212 if (checkedBox) {
213 console.log('Чекбокс уже отмечен');
214 return false;
215 }
216
217 // Ищем label чекбокса для клика
218 const checkboxLabel = row.querySelector('[data-testid*="Checkbox.label"]');
219 if (checkboxLabel) {
220 checkboxLabel.click();
221 console.log('Клик по чекбоксу выполнен');
222 return true;
223 }
224
225 console.log('Чекбокс не найден в строке');
226 return false;
227 }
228
229 // Функция для скроллинга таблицы
230 function scrollTable() {
231 // Ищем контейнер со скроллом - это div.dc-Scrollbar
232 const scrollContainer = document.querySelector('[data-testid="StatisticsReportTable"] .dc-Scrollbar');
233
234 if (scrollContainer) {
235 scrollContainer.scrollTop += 300;
236 console.log('Скролл выполнен, scrollTop:', scrollContainer.scrollTop);
237 return scrollContainer.scrollTop;
238 }
239
240 console.log('Контейнер для скролла не найден');
241 return 0;
242 }
243
244 // Функция для проверки, достигнут ли конец таблицы
245 function isScrollAtBottom() {
246 const scrollContainer = document.querySelector('[data-testid="StatisticsReportTable"] .dc-Scrollbar');
247
248 if (scrollContainer) {
249 const isAtBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop <= scrollContainer.clientHeight + 100;
250 console.log('Проверка конца таблицы:', isAtBottom, 'scrollHeight:', scrollContainer.scrollHeight, 'scrollTop:', scrollContainer.scrollTop, 'clientHeight:', scrollContainer.clientHeight);
251 return isAtBottom;
252 }
253 return false;
254 }
255
256 // Основная функция обработки
257 async function startProcessing(masksText, statusDiv, startButton, stopButton, cancelButton) {
258 const masks = masksText.split('\n').map(m => m.trim()).filter(m => m.length > 0);
259
260 if (masks.length === 0) {
261 updateStatus(statusDiv, 'Ошибка: не указаны маски', 'error');
262 return;
263 }
264
265 console.log('Начинаем обработку с масками:', masks);
266
267 // Переключаем кнопки
268 startButton.style.display = 'none';
269 stopButton.style.display = 'block';
270 cancelButton.disabled = true;
271 cancelButton.style.opacity = '0.5';
272 cancelButton.style.cursor = 'not-allowed';
273
274 updateStatus(statusDiv, 'Начинаем обработку...', 'info');
275
276 let processedCount = 0;
277 let selectedCount = 0;
278 let previousScrollTop = -1;
279 let sameScrollIterations = 0;
280 let processLoop = null;
281
282 // Функция остановки
283 const stopProcessing = () => {
284 if (processLoop) {
285 clearInterval(processLoop);
286 processLoop = null;
287 }
288
289 updateStatus(statusDiv, `⏸ Остановлено. Обработано: ${processedCount}, выбрано: ${selectedCount}`, 'info');
290 console.log('Обработка остановлена пользователем');
291
292 // Возвращаем кнопки в исходное состояние
293 stopButton.style.display = 'none';
294 cancelButton.disabled = false;
295 cancelButton.style.opacity = '1';
296 cancelButton.style.cursor = 'pointer';
297 cancelButton.textContent = 'Закрыть';
298 };
299
300 stopButton.onclick = stopProcessing;
301
302 // Функция для обработки видимых строк
303 const processVisibleRows = () => {
304 const rows = getTableRows();
305 console.log(`Обрабатываем ${rows.length} строк`);
306
307 rows.forEach(row => {
308 const platformName = getPlatformName(row);
309 if (!platformName) return;
310
311 // Проверяем, содержит ли название хотя бы одну маску
312 const matchesMask = masks.some(mask => platformName.includes(mask));
313
314 if (matchesMask) {
315 const clicked = clickCheckbox(row);
316 if (clicked) {
317 selectedCount++;
318 console.log(`Выбрана площадка: ${platformName}`);
319 }
320 }
321 processedCount++;
322 });
323
324 updateStatus(statusDiv, `Обработано строк: ${processedCount}, выбрано: ${selectedCount}`, 'info');
325 };
326
327 // Основной цикл скроллинга и обработки
328 processLoop = setInterval(() => {
329 processVisibleRows();
330
331 const currentScrollTop = scrollTable();
332
333 // Проверяем, изменилась ли позиция скролла
334 if (currentScrollTop === previousScrollTop) {
335 sameScrollIterations++;
336 console.log(`Скролл не изменился, попытка ${sameScrollIterations} из 5`);
337 } else {
338 sameScrollIterations = 0;
339 previousScrollTop = currentScrollTop;
340 }
341
342 // Если достигли конца или застряли на одном месте 5 раз
343 if (isScrollAtBottom() || sameScrollIterations >= 5) {
344 clearInterval(processLoop);
345
346 // Финальная обработка
347 processVisibleRows();
348
349 updateStatus(statusDiv, `✓ Все площадки выделены! Обработано: ${processedCount}, выбрано: ${selectedCount}`, 'success');
350 console.log('Обработка завершена');
351
352 // Скрываем кнопку стоп и включаем кнопку закрытия
353 stopButton.style.display = 'none';
354 cancelButton.disabled = false;
355 cancelButton.style.opacity = '1';
356 cancelButton.style.cursor = 'pointer';
357 cancelButton.textContent = 'Закрыть';
358 }
359 }, 500);
360 }
361
362 // Функция для создания кнопки "Удалить площадки"
363 function createMainButton() {
364 const pageHead = document.querySelector('[data-testid="ReportsWizardLoginPage.PageHead"]');
365 if (!pageHead) {
366 console.log('PageHead не найден, повторим попытку позже');
367 return false;
368 }
369
370 // Проверяем, не создана ли уже кнопка
371 if (document.getElementById('platform-remover-button')) {
372 console.log('Кнопка уже существует');
373 return true;
374 }
375
376 const titleContainer = pageHead.querySelector('.dc-Stack.dc-Stack_type_horizontal.dc-Stack_gap_8');
377 if (!titleContainer) {
378 console.log('Контейнер заголовка не найден');
379 return false;
380 }
381
382 const button = document.createElement('button');
383 button.id = 'platform-remover-button';
384 button.textContent = 'Удалить площадки';
385 button.style.cssText = `
386 padding: 8px 16px;
387 background: #fc0;
388 color: #000;
389 border: none;
390 border-radius: 6px;
391 font-size: 14px;
392 font-weight: 600;
393 cursor: pointer;
394 transition: background 0.2s;
395 margin-left: 12px;
396 `;
397 button.onmouseover = () => button.style.background = '#ffdb4d';
398 button.onmouseout = () => button.style.background = '#fc0';
399 button.onclick = createModal;
400
401 titleContainer.appendChild(button);
402 console.log('Кнопка "Удалить площадки" добавлена');
403 return true;
404 }
405
406 // Инициализация
407 function init() {
408 console.log('Инициализация расширения');
409
410 // Пробуем создать кнопку сразу
411 if (!createMainButton()) {
412 // Если не получилось, ждем загрузки страницы
413 const observer = new MutationObserver(debounce(() => {
414 if (createMainButton()) {
415 observer.disconnect();
416 }
417 }, 500));
418
419 observer.observe(document.body, {
420 childList: true,
421 subtree: true
422 });
423 }
424 }
425
426 // Запускаем после загрузки DOM
427 if (document.readyState === 'loading') {
428 document.addEventListener('DOMContentLoaded', init);
429 } else {
430 init();
431 }
432})();