DTF User Notes

Add notes to users on dtf.ru

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