Yandex Direct - Удаление площадок по маскам

Автоматическое выделение и удаление площадок по заданным маскам

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})();