DTF User Notes

Add notes to users on dtf.ru

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

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