DTF User Notes

Add notes to users on dtf.ru

目前为 2025-04-02 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name DTF User Notes
  3. // @version 1.1
  4. // @description Add notes to users on dtf.ru
  5. // @author Avicenna
  6. // @match https://dtf.ru/*
  7. // @license MIT
  8. // @grant none
  9. // @namespace http://tampermonkey.net/
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Функция для проверки, является ли текущая страница профилем пользователя
  16. function isUserProfilePage() {
  17. const path = window.location.pathname;
  18. return /^\/u\/\d+-[^\/]+(\/comments)?$/.test(path);
  19. }
  20.  
  21. // Функция для создания заметки
  22. function createNote(userId, username) {
  23. const currentNote = localStorage.getItem(`note_${userId}`);
  24. const currentColor = localStorage.getItem(`note_color_${userId}`) || 'gray';
  25.  
  26. const noteText = prompt(`Введите заметку для пользователя ${username}:`, currentNote || '');
  27. if (noteText === null) return;
  28.  
  29. const useRedText = confirm("Сделать текст заметки красным? (ОК - да, Отмена - нет)");
  30. const textColor = useRedText ? 'red' : 'gray';
  31.  
  32. localStorage.setItem(`note_${userId}`, noteText);
  33. localStorage.setItem(`note_color_${userId}`, textColor);
  34. displayUserNotes();
  35. }
  36.  
  37. // Функция для извлечения ID пользователя из ссылки
  38. function extractUserId(href) {
  39. const match = href.match(/\/u\/(\d+)-/);
  40. return match ? match[1] : null;
  41. }
  42.  
  43. // Функция для отображения заметок рядом с ником пользователя
  44. function displayUserNotes() {
  45. // Отображение заметок в постах и комментариях
  46. const authors = document.querySelectorAll('.author__name, .comment__author, .content-header__author a');
  47. authors.forEach(author => {
  48. if (author.href) {
  49. const userId = extractUserId(author.href);
  50. if (userId) {
  51. const note = localStorage.getItem(`note_${userId}`);
  52. const textColor = localStorage.getItem(`note_color_${userId}`) || 'gray';
  53.  
  54. if (note) {
  55. // Удаляем старую заметку, если она есть
  56. const existingNote = author.parentNode.querySelector('.user-note');
  57. if (existingNote) existingNote.remove();
  58.  
  59. const noteSpan = document.createElement('span');
  60. noteSpan.innerText = ` ${note}`;
  61. noteSpan.classList.add('user-note');
  62. noteSpan.style.color = textColor;
  63. noteSpan.style.marginLeft = '5px';
  64. noteSpan.style.padding = '2px 5px';
  65. noteSpan.style.borderRadius = '3px';
  66.  
  67. author.parentNode.insertBefore(noteSpan, author.nextSibling);
  68. }
  69. }
  70. }
  71. });
  72.  
  73. // Отображение заметки в профиле
  74. const profileName = document.querySelector('.subsite-card__name h1');
  75. if (profileName) {
  76. const userId = extractUserId(window.location.pathname);
  77. if (userId) {
  78. const note = localStorage.getItem(`note_${userId}`);
  79. const textColor = localStorage.getItem(`note_color_${userId}`) || 'gray';
  80.  
  81. if (note) {
  82. // Удаляем старую заметку, если она есть
  83. const existingNote = profileName.parentNode.querySelector('.user-note');
  84. if (existingNote) existingNote.remove();
  85.  
  86. const noteSpan = document.createElement('span');
  87. noteSpan.innerText = ` ${note}`;
  88. noteSpan.classList.add('user-note');
  89. noteSpan.style.color = textColor;
  90. noteSpan.style.marginLeft = '5px';
  91. noteSpan.style.padding = '2px 5px';
  92. noteSpan.style.borderRadius = '3px';
  93.  
  94. profileName.parentNode.insertBefore(noteSpan, profileName.nextSibling);
  95. }
  96. }
  97. }
  98. }
  99.  
  100. // Функция для экспорта заметок в JSON
  101. function exportNotes() {
  102. const notes = {};
  103. for (let i = 0; i < localStorage.length; i++) {
  104. const key = localStorage.key(i);
  105. if (key.startsWith('note_') && !key.startsWith('note_color_')) {
  106. const userId = key.replace('note_', '');
  107. const note = localStorage.getItem(key);
  108. const color = localStorage.getItem(`note_color_${userId}`) || 'gray';
  109. notes[userId] = { note, color };
  110. }
  111. }
  112.  
  113. const json = JSON.stringify(notes, null, 2);
  114. const blob = new Blob([json], { type: 'application/json' });
  115. const url = URL.createObjectURL(blob);
  116.  
  117. // Форматируем дату и время для имени файла
  118. const now = new Date();
  119. const formattedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
  120. const formattedTime = `${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`;
  121. const fileName = `dtf_notes_${formattedDate}_${formattedTime}.json`;
  122.  
  123. const a = document.createElement('a');
  124. a.href = url;
  125. a.download = fileName;
  126. a.click();
  127. URL.revokeObjectURL(url);
  128. }
  129.  
  130. // Функция для импорта заметок из JSON
  131. function importNotes(event) {
  132. const file = event.target.files[0];
  133. if (!file) return;
  134.  
  135. // Предупреждение перед импортом
  136. const warningMessage = `
  137. Внимание! Импорт удалит все текущие заметки.
  138. Убедитесь, что у вас есть резервная копия.
  139. Продолжить?
  140. `;
  141.  
  142. const isConfirmed = confirm(warningMessage);
  143. if (!isConfirmed) return;
  144.  
  145. const reader = new FileReader();
  146. reader.onload = (e) => {
  147. const json = e.target.result;
  148. const notes = JSON.parse(json);
  149.  
  150. // Очищаем все существующие заметки
  151. for (let i = 0; i < localStorage.length; i++) {
  152. const key = localStorage.key(i);
  153. if (key.startsWith('note_') || key.startsWith('note_color_')) {
  154. localStorage.removeItem(key);
  155. }
  156. }
  157.  
  158. // Добавляем только те заметки, которые есть в файле
  159. for (const userId in notes) {
  160. if (notes.hasOwnProperty(userId)) {
  161. localStorage.setItem(`note_${userId}`, notes[userId].note);
  162. localStorage.setItem(`note_color_${userId}`, notes[userId].color);
  163. }
  164. }
  165.  
  166. displayUserNotes();
  167. };
  168. reader.readAsText(file);
  169. }
  170.  
  171. // Функция для проверки, есть ли в меню пункт "Заблокировать"
  172. function hasBlockOption(menu) {
  173. const options = menu.querySelectorAll('.context-list-option__label');
  174. for (let option of options) {
  175. if (option.textContent.trim() === 'Заблокировать') {
  176. return true;
  177. }
  178. }
  179. return false;
  180. }
  181.  
  182. // Функция для добавления кнопок в существующее выпадающее меню
  183. function addButtonsToExistingMenu() {
  184. const existingMenus = document.querySelectorAll('.context-list');
  185.  
  186. existingMenus.forEach(menu => {
  187. // Проверяем, что это меню профиля пользователя и есть пункт "Заблокировать"
  188. if (!isUserProfilePage() || !hasBlockOption(menu)) return;
  189.  
  190. // Проверяем, что кнопки еще не добавлены
  191. if (menu.querySelector('.custom-note-button')) return;
  192.  
  193. // Получаем имя пользователя из страницы
  194. const usernameElement = document.querySelector('.subsite-card__name h1');
  195. if (!usernameElement) return;
  196. const username = usernameElement.textContent;
  197.  
  198. // Получаем ID пользователя из URL
  199. const userId = extractUserId(window.location.pathname);
  200. if (!userId) return;
  201.  
  202. // Создаем кнопку "Добавить заметку"
  203. const addNoteOption = document.createElement('div');
  204. addNoteOption.classList.add('context-list-option', 'context-list-option--with-art', 'custom-note-button');
  205. addNoteOption.style = '--press-duration: 140ms;';
  206. addNoteOption.innerHTML = `
  207. <div class="context-list-option__art context-list-option__art--icon">
  208. <svg class="icon icon--note" width="20" height="20"><use xlink:href="#note"></use></svg>
  209. </div>
  210. <div class="context-list-option__label">Добавить заметку</div>
  211. `;
  212. addNoteOption.onclick = () => createNote(userId, username);
  213.  
  214. // Создаем кнопку "Экспорт заметок"
  215. const exportOption = document.createElement('div');
  216. exportOption.classList.add('context-list-option', 'context-list-option--with-art', 'custom-note-button');
  217. exportOption.style = '--press-duration: 140ms;';
  218. exportOption.innerHTML = `
  219. <div class="context-list-option__art context-list-option__art--icon">
  220. <svg class="icon icon--export" width="20" height="20"><use xlink:href="#export"></use></svg>
  221. </div>
  222. <div class="context-list-option__label">Экспорт заметок</div>
  223. `;
  224. exportOption.onclick = exportNotes;
  225.  
  226. // Создаем кнопку "Импорт заметок"
  227. const importOption = document.createElement('div');
  228. importOption.classList.add('context-list-option', 'context-list-option--with-art', 'custom-note-button');
  229. importOption.style = '--press-duration: 140ms;';
  230. importOption.innerHTML = `
  231. <div class="context-list-option__art context-list-option__art--icon">
  232. <svg class="icon icon--import" width="20" height="20"><use xlink:href="#import"></use></svg>
  233. </div>
  234. <div class="context-list-option__label">Импорт заметок</div>
  235. `;
  236. importOption.onclick = () => {
  237. const importInput = document.createElement('input');
  238. importInput.type = 'file';
  239. importInput.accept = '.json';
  240. importInput.style.display = 'none';
  241. importInput.onchange = importNotes;
  242. importInput.click();
  243. };
  244.  
  245. // Добавляем разделитель перед нашими кнопками
  246. const separator = document.createElement('div');
  247. separator.style.height = '1px';
  248. separator.style.backgroundColor = 'var(--color-border)';
  249. separator.style.margin = '4px 0';
  250.  
  251. // Добавляем элементы в меню
  252. menu.appendChild(separator);
  253. menu.appendChild(addNoteOption);
  254. menu.appendChild(exportOption);
  255. menu.appendChild(importOption);
  256. });
  257. }
  258.  
  259. // Функция для запуска после загрузки DOM
  260. function init() {
  261. if (document.querySelector('.subsite-card__header')) {
  262. displayUserNotes();
  263. addButtonsToExistingMenu();
  264. }
  265. }
  266.  
  267. // Оптимизация: debounce для вызова displayUserNotes
  268. let debounceTimer;
  269. function debounceDisplayUserNotes() {
  270. clearTimeout(debounceTimer);
  271. debounceTimer = setTimeout(() => displayUserNotes(), 300); // Задержка 300 мс
  272. }
  273.  
  274. // Отслеживание изменений на странице
  275. const observer = new MutationObserver((mutationsList) => {
  276. for (let mutation of mutationsList) {
  277. if (mutation.type === 'childList') {
  278. if (document.querySelector('.subsite-card__header')) {
  279. addButtonsToExistingMenu();
  280. }
  281.  
  282. // Вызываем displayUserNotes с задержкой
  283. debounceDisplayUserNotes();
  284. }
  285. }
  286. });
  287.  
  288. // Начинаем наблюдение за изменениями в DOM
  289. observer.observe(document.body, { childList: true, subtree: true });
  290.  
  291. // Запуск функций при загрузке страницы
  292. init();
  293. })();