Greasy Fork 还支持 简体中文。

DTF User Notes

Add notes to users on dtf.ru

目前為 2025-02-26 提交的版本,檢視 最新版本

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