DTF User Notes

Add notes to users on dtf.ru

Verze ze dne 26. 02. 2025. Zobrazit nejnovější verzi.

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

(function() {
    'use strict';

    // Функция для создания заметки
    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 addButtonsToExistingMenu() {
        const existingMenu = document.querySelector('.context-list');
        if (!existingMenu || existingMenu.querySelector('.custom-note-button')) 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 = () => {
            const userId = extractUserId(window.location.pathname);
            const username = document.querySelector('.subsite-card__name h1').innerText;
            if (userId) 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();
        };

        // Добавляем кнопки в меню
        existingMenu.appendChild(addNoteOption);
        existingMenu.appendChild(exportOption);
        existingMenu.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();
})();