DTF User Notes

Add notes to users on dtf.ru

目前為 2025-04-02 提交的版本,檢視 最新版本

// ==UserScript==
// @name         DTF User Notes
// @version      1.1
// @description  Add notes to users on dtf.ru
// @author       Avicenna
// @match        https://dtf.ru/*
// @license      MIT
// @grant        none
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    // Функция для проверки, является ли текущая страница профилем пользователя
    function isUserProfilePage() {
        const path = window.location.pathname;
        return /^\/u\/\d+-[^\/]+(\/comments)?$/.test(path);
    }

    // Функция для создания заметки
    function createNote(userId, username) {
        const currentNote = localStorage.getItem(`note_${userId}`);
        const currentColor = localStorage.getItem(`note_color_${userId}`) || 'gray';

        const noteText = prompt(`Введите заметку для пользователя ${username}:`, currentNote || '');
        if (noteText === null) return;

        const useRedText = confirm("Сделать текст заметки красным? (ОК - да, Отмена - нет)");
        const textColor = useRedText ? 'red' : 'gray';

        localStorage.setItem(`note_${userId}`, noteText);
        localStorage.setItem(`note_color_${userId}`, textColor);
        displayUserNotes();
    }

    // Функция для извлечения ID пользователя из ссылки
    function extractUserId(href) {
        const match = href.match(/\/u\/(\d+)-/);
        return match ? match[1] : null;
    }

    // Функция для отображения заметок рядом с ником пользователя
    function displayUserNotes() {
        // Отображение заметок в постах и комментариях
        const authors = document.querySelectorAll('.author__name, .comment__author, .content-header__author a');
        authors.forEach(author => {
            if (author.href) {
                const userId = extractUserId(author.href);
                if (userId) {
                    const note = localStorage.getItem(`note_${userId}`);
                    const textColor = localStorage.getItem(`note_color_${userId}`) || 'gray';

                    if (note) {
                        // Удаляем старую заметку, если она есть
                        const existingNote = author.parentNode.querySelector('.user-note');
                        if (existingNote) existingNote.remove();

                        const noteSpan = document.createElement('span');
                        noteSpan.innerText = ` ${note}`;
                        noteSpan.classList.add('user-note');
                        noteSpan.style.color = textColor;
                        noteSpan.style.marginLeft = '5px';
                        noteSpan.style.padding = '2px 5px';
                        noteSpan.style.borderRadius = '3px';

                        author.parentNode.insertBefore(noteSpan, author.nextSibling);
                    }
                }
            }
        });

        // Отображение заметки в профиле
        const profileName = document.querySelector('.subsite-card__name h1');
        if (profileName) {
            const userId = extractUserId(window.location.pathname);
            if (userId) {
                const note = localStorage.getItem(`note_${userId}`);
                const textColor = localStorage.getItem(`note_color_${userId}`) || 'gray';

                if (note) {
                    // Удаляем старую заметку, если она есть
                    const existingNote = profileName.parentNode.querySelector('.user-note');
                    if (existingNote) existingNote.remove();

                    const noteSpan = document.createElement('span');
                    noteSpan.innerText = ` ${note}`;
                    noteSpan.classList.add('user-note');
                    noteSpan.style.color = textColor;
                    noteSpan.style.marginLeft = '5px';
                    noteSpan.style.padding = '2px 5px';
                    noteSpan.style.borderRadius = '3px';

                    profileName.parentNode.insertBefore(noteSpan, profileName.nextSibling);
                }
            }
        }
    }

    // Функция для экспорта заметок в JSON
    function exportNotes() {
        const notes = {};
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key.startsWith('note_') && !key.startsWith('note_color_')) {
                const userId = key.replace('note_', '');
                const note = localStorage.getItem(key);
                const color = localStorage.getItem(`note_color_${userId}`) || 'gray';
                notes[userId] = { note, color };
            }
        }

        const json = JSON.stringify(notes, null, 2);
        const blob = new Blob([json], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        // Форматируем дату и время для имени файла
        const now = new Date();
        const formattedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
        const formattedTime = `${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`;
        const fileName = `dtf_notes_${formattedDate}_${formattedTime}.json`;

        const a = document.createElement('a');
        a.href = url;
        a.download = fileName;
        a.click();
        URL.revokeObjectURL(url);
    }

    // Функция для импорта заметок из JSON
    function importNotes(event) {
        const file = event.target.files[0];
        if (!file) return;

        // Предупреждение перед импортом
        const warningMessage = `
            Внимание! Импорт удалит все текущие заметки.
            Убедитесь, что у вас есть резервная копия.
            Продолжить?
        `;

        const isConfirmed = confirm(warningMessage);
        if (!isConfirmed) return;

        const reader = new FileReader();
        reader.onload = (e) => {
            const json = e.target.result;
            const notes = JSON.parse(json);

            // Очищаем все существующие заметки
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                if (key.startsWith('note_') || key.startsWith('note_color_')) {
                    localStorage.removeItem(key);
                }
            }

            // Добавляем только те заметки, которые есть в файле
            for (const userId in notes) {
                if (notes.hasOwnProperty(userId)) {
                    localStorage.setItem(`note_${userId}`, notes[userId].note);
                    localStorage.setItem(`note_color_${userId}`, notes[userId].color);
                }
            }

            displayUserNotes();
        };
        reader.readAsText(file);
    }

    // Функция для проверки, есть ли в меню пункт "Заблокировать"
    function hasBlockOption(menu) {
        const options = menu.querySelectorAll('.context-list-option__label');
        for (let option of options) {
            if (option.textContent.trim() === 'Заблокировать') {
                return true;
            }
        }
        return false;
    }

    // Функция для добавления кнопок в существующее выпадающее меню
    function addButtonsToExistingMenu() {
        const existingMenus = document.querySelectorAll('.context-list');

        existingMenus.forEach(menu => {
            // Проверяем, что это меню профиля пользователя и есть пункт "Заблокировать"
            if (!isUserProfilePage() || !hasBlockOption(menu)) return;

            // Проверяем, что кнопки еще не добавлены
            if (menu.querySelector('.custom-note-button')) return;

            // Получаем имя пользователя из страницы
            const usernameElement = document.querySelector('.subsite-card__name h1');
            if (!usernameElement) return;
            const username = usernameElement.textContent;

            // Получаем ID пользователя из URL
            const userId = extractUserId(window.location.pathname);
            if (!userId) return;

            // Создаем кнопку "Добавить заметку"
            const addNoteOption = document.createElement('div');
            addNoteOption.classList.add('context-list-option', 'context-list-option--with-art', 'custom-note-button');
            addNoteOption.style = '--press-duration: 140ms;';
            addNoteOption.innerHTML = `
                <div class="context-list-option__art context-list-option__art--icon">
                    <svg class="icon icon--note" width="20" height="20"><use xlink:href="#note"></use></svg>
                </div>
                <div class="context-list-option__label">Добавить заметку</div>
            `;
            addNoteOption.onclick = () => createNote(userId, username);

            // Создаем кнопку "Экспорт заметок"
            const exportOption = document.createElement('div');
            exportOption.classList.add('context-list-option', 'context-list-option--with-art', 'custom-note-button');
            exportOption.style = '--press-duration: 140ms;';
            exportOption.innerHTML = `
                <div class="context-list-option__art context-list-option__art--icon">
                    <svg class="icon icon--export" width="20" height="20"><use xlink:href="#export"></use></svg>
                </div>
                <div class="context-list-option__label">Экспорт заметок</div>
            `;
            exportOption.onclick = exportNotes;

            // Создаем кнопку "Импорт заметок"
            const importOption = document.createElement('div');
            importOption.classList.add('context-list-option', 'context-list-option--with-art', 'custom-note-button');
            importOption.style = '--press-duration: 140ms;';
            importOption.innerHTML = `
                <div class="context-list-option__art context-list-option__art--icon">
                    <svg class="icon icon--import" width="20" height="20"><use xlink:href="#import"></use></svg>
                </div>
                <div class="context-list-option__label">Импорт заметок</div>
            `;
            importOption.onclick = () => {
                const importInput = document.createElement('input');
                importInput.type = 'file';
                importInput.accept = '.json';
                importInput.style.display = 'none';
                importInput.onchange = importNotes;
                importInput.click();
            };

            // Добавляем разделитель перед нашими кнопками
            const separator = document.createElement('div');
            separator.style.height = '1px';
            separator.style.backgroundColor = 'var(--color-border)';
            separator.style.margin = '4px 0';

            // Добавляем элементы в меню
            menu.appendChild(separator);
            menu.appendChild(addNoteOption);
            menu.appendChild(exportOption);
            menu.appendChild(importOption);
        });
    }

    // Функция для запуска после загрузки DOM
    function init() {
        if (document.querySelector('.subsite-card__header')) {
            displayUserNotes();
            addButtonsToExistingMenu();
        }
    }

    // Оптимизация: debounce для вызова displayUserNotes
    let debounceTimer;
    function debounceDisplayUserNotes() {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => displayUserNotes(), 300); // Задержка 300 мс
    }

    // Отслеживание изменений на странице
    const observer = new MutationObserver((mutationsList) => {
        for (let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                if (document.querySelector('.subsite-card__header')) {
                    addButtonsToExistingMenu();
                }

                // Вызываем displayUserNotes с задержкой
                debounceDisplayUserNotes();
            }
        }
    });

    // Начинаем наблюдение за изменениями в DOM
    observer.observe(document.body, { childList: true, subtree: true });

    // Запуск функций при загрузке страницы
    init();
})();