DTF User Notes

Add notes to users on dtf.ru

当前为 2025-04-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         DTF User Notes
// @version      1.2
// @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+-[^\/]+|id\d+|[\w-]+)(\/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 пользователя из ссылки или JSON-LD
    function extractUserId(hrefOrPath) {
        // Проверяем старый формат ссылки: /u/12345-username
        const oldFormatMatch = hrefOrPath.match(/\/u\/(\d+)-/);
        if (oldFormatMatch) return oldFormatMatch[1];

        // Проверяем новый формат ссылки: /id12345
        const newFormatMatch = hrefOrPath.match(/\/id(\d+)/);
        if (newFormatMatch) return newFormatMatch[1];

        // Для кастомных ссылок (/username) ищем JSON-LD блок
        if (/^\/([\w-]+)$/.test(hrefOrPath)) {
            const jsonLdScript = document.querySelector('script[type="application/ld+json"][data-hid]');
            if (jsonLdScript) {
                try {
                    const jsonLd = JSON.parse(jsonLdScript.textContent);
                    if (jsonLd["@graph"] && jsonLd["@graph"][0] && jsonLd["@graph"][0].mainEntity) {
                        return jsonLd["@graph"][0].mainEntity.identifier.toString();
                    }
                } catch (e) {
                    console.error("Error parsing JSON-LD:", e);
                }
            }
        }

        return null;
    }

    // Функция для отображения заметок рядом с ником пользователя
    function displayUserNotes() {
        // Отображение заметок в постах и комментариях
        const authors = document.querySelectorAll('.author__name, .comment__author, .content-header__author a, .user-link, .comment-item__user-link');
        authors.forEach(author => {
            if (author.href || author.getAttribute('data-router-link')) {
                const href = author.href || author.getAttribute('data-router-link');
                const userId = extractUserId(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 или JSON-LD
            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();
})();