DTF Enhancer

Выводит список подписок в сайдбаре и раскрывает список комментов

目前为 2024-04-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name DTF Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.2
  5. // @description Выводит список подписок в сайдбаре и раскрывает список комментов
  6. // @author You
  7. // @match *://dtf.ru/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=dtf.ru
  9. // @run-at document-end
  10. // @grant GM.getValue
  11. // @grant GM.setValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. const USE_WIDE_LAYOUT = 0; // Показывать страницу на всю ширину. 0 - выкл, 1 - вкл
  17. const CONTENT_WIDTH = '800px'; // Ширина контентной области. Можно задать любое значение, например: 960px или 1200px
  18. const SHOW_SCROLL_UP_BUTTON = 1; // Показывать кнопку "вверх". 0 - выкл, 1 - вкл
  19. const ENABLE_AUTO_EXPAND_COMMENTS = 1; // Автоматически раскрывать общий список комментариев. 0 - выкл, 1 - вкл
  20.  
  21. const dict = {" ":"-","Ё":"YO","Й":"I","Ц":"TS","У":"U","К":"K","Е":"E","Н":"N","Г":"G","Ш":"SH","Щ":"SCH","З":"Z","Х":"H","Ъ":"'","ё":"yo","й":"i","ц":"ts","у":"u","к":"k","е":"e","н":"n","г":"g","ш":"sh","щ":"sch","з":"z","х":"h","ъ":"'","Ф":"F","Ы":"I","В":"V","А":"A","П":"P","Р":"R","О":"O","Л":"L","Д":"D","Ж":"ZH","Э":"E","ф":"f","ы":"i","в":"v","а":"a","п":"p","р":"r","о":"o","л":"l","д":"d","ж":"zh","э":"e","Я":"Ya","Ч":"CH","С":"S","М":"M","И":"I","Т":"T","Ь":"'","Б":"B","Ю":"YU","я":"ya","ч":"ch","с":"s","м":"m","и":"i","т":"t","ь":"'","б":"b","ю":"yu"};
  22.  
  23. const transliterate = (word) => word.split('').map((char) => dict[char] || char).join("");
  24.  
  25. const cn = (tagName, attrs = {}, childrenList = [], parentNode = null) => {
  26. const node = document.createElement(tagName);
  27.  
  28. if (typeof attrs === 'object') {
  29. for (const attrsKey in attrs) node.setAttribute(attrsKey, attrs[attrsKey]);
  30. }
  31.  
  32. if (Array.isArray(childrenList)) {
  33. childrenList.forEach(child => {
  34. node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
  35. });
  36. }
  37.  
  38. if (parentNode) {
  39. parentNode.appendChild(node);
  40. }
  41.  
  42. return node;
  43. };
  44.  
  45. const getDomElementAsync = (selector, timerLimit = 10000) => {
  46. return new Promise((resolve, reject) => {
  47. try {
  48. setTimeout(() => {
  49. console.log(`Время ожидания DOM элемента ${selector} истекло (${timerLimit / 1000}s)`);
  50. resolve(null);
  51. }, timerLimit);
  52.  
  53. let timerId;
  54.  
  55. const tick = () => {
  56. const element = document.querySelector(selector);
  57.  
  58. if (element) {
  59. clearTimeout(timerId);
  60. resolve(element);
  61. } else {
  62. timerId = setTimeout(tick, 100);
  63. }
  64. };
  65.  
  66. tick();
  67. } catch (e) {
  68. reject(e);
  69. }
  70. });
  71. };
  72.  
  73. const debounce = (func, wait) => {
  74. let timeout;
  75. return function (...args) {
  76. return new Promise(resolve => {
  77. clearTimeout(timeout);
  78. timeout = setTimeout(() => {
  79. timeout = null;
  80. Promise.resolve(func.apply(this, [...args])).then(resolve);
  81. }, wait);
  82. });
  83. };
  84. };
  85.  
  86. const observeUrlChange = async (onChange) => {
  87. await GM.setValue('currentUrl', window.location.href);
  88.  
  89. const onChangeHandler = async () => {
  90. const oldHref = await GM.getValue('currentUrl');
  91. const newHref = window.location.href;
  92.  
  93. if (oldHref !== newHref) {
  94. console.log('observeUrlChange');
  95.  
  96. await GM.setValue('currentUrl', newHref);
  97. onChange?.();
  98. }
  99. };
  100.  
  101. const debouncedOnChangeHandler = debounce(onChangeHandler, 500);
  102.  
  103. const observer = new MutationObserver(debouncedOnChangeHandler);
  104.  
  105. observer.observe(document.body, {
  106. childList: true,
  107. subtree: true,
  108. });
  109. };
  110.  
  111. const injectStyles = () => {
  112. const styles = `
  113. .sidebar-subs {
  114. display: flex;
  115. flex-direction: column;
  116. overflow: auto;
  117. margin: 24px 0;
  118. }
  119.  
  120. .sidebar-sibs__title {
  121. margin-bottom: 16px;
  122. padding: 0 8px;
  123. font-weight: 500;
  124. font-size: 18px;
  125. }
  126.  
  127. .sidebar-sibs__list {
  128. padding: 0;
  129. }
  130.  
  131. .sidebar-item._sub img.icon {
  132. width: 24px;
  133. border-radius: 50%;
  134. }
  135.  
  136. .sidebar-item._sub span {
  137. white-space: nowrap;
  138. overflow: hidden;
  139. text-overflow: ellipsis;
  140. min-width: 1px;
  141. }
  142.  
  143. /* перебиваем стили DTF */
  144. .layout,
  145. .header__layout {
  146. display: flex;
  147. gap: 24px;
  148. ${USE_WIDE_LAYOUT ? 'max-width: none;' : ''}
  149. ${SHOW_SCROLL_UP_BUTTON ? 'padding: 0 40px;' : ''}
  150. }
  151.  
  152. .header__left,
  153. .header__right,
  154. .aside {
  155. width: 220px;
  156. flex-shrink: 0;
  157. }
  158.  
  159. .header__right {
  160. justify-content: flex-end;
  161. margin-left: 0;
  162. }
  163.  
  164. .view,
  165. .header__search {
  166. width: 100%;
  167. max-width: ${CONTENT_WIDTH};
  168. }
  169.  
  170. .sidebar__main {
  171. display: flex;
  172. flex-direction: column;
  173. flex-shrink: 0;
  174. min-width: 1px;
  175. overflow: auto;
  176. }
  177.  
  178. .sidebar-item {
  179. flex-shrink: 0;
  180. }
  181.  
  182. .sidebar-editor-button {
  183. margin-top: 24px;
  184. }
  185.  
  186. .sidebar-editor-buttons {
  187. margin-top: auto;
  188. margin-bottom: 16px;
  189. }
  190.  
  191. .account-menu {
  192. visibility: hidden;
  193. }
  194.  
  195. body.dtf-subs-script-inited .account-menu {
  196. visibility: visible;
  197. }
  198.  
  199. /* кнопка "вверх" */
  200. .scroll-up-button {
  201. display: flex;
  202. justify-content: center;
  203. position: fixed;
  204. top: var(--layout-header-height);;
  205. left: 0;
  206. width: 40px;
  207. height: 100vh;
  208. padding-top: 10px;
  209. background-color: transparent;
  210. cursor: pointer;
  211. z-index: var(--layout-z-index-header);
  212. opacity: 0;
  213. pointer-events: none;
  214. transition: background-color 200ms ease-out, opacity 200ms ease-out;
  215. }
  216.  
  217. [data-theme="light"] .scroll-up-button:hover {
  218. background-color: var(--theme-color-brand-header);
  219. }
  220.  
  221. [data-theme="dark"] .scroll-up-button:hover {
  222. background-color: rgba(255, 255, 255, .1);
  223. }
  224.  
  225. .scroll-up-button use {
  226. display: none;
  227. }
  228.  
  229. .scroll-up-button.up use:nth-child(1) {
  230. display: block;
  231. }
  232.  
  233. .scroll-up-button.down use:nth-child(2) {
  234. display: block;
  235. }
  236.  
  237. .scroll-up-button.visible {
  238. opacity: 1;
  239. pointer-events: all;
  240. }
  241. `;
  242.  
  243. document.head.insertAdjacentHTML("beforeend", `<style type="text/css" id="dtfSubsStyles">${styles}</style>`)
  244. };
  245.  
  246. const fetchSubs = async (userId) => {
  247. const resp = await fetch(`https://api.dtf.ru/v2.5/subsite/subscriptions?subsiteId=${userId}`);
  248. const { result } = await resp.json();
  249.  
  250. return result.items;
  251. }
  252.  
  253. const getImageUrl = (uuid) => `https://leonardo.osnova.io/${uuid}/-/scale_crop/32x32/`;
  254.  
  255. const createSidebarItem = (name, imageId, href) => {
  256. const imgEl = cn('img', { class: 'icon', src: getImageUrl(imageId) });
  257. const nameEl = cn('span', {}, [name]);
  258. const result = cn('a', { class: 'sidebar-item _sub', href: transliterate(href), alt: name }, [imgEl, nameEl]);
  259.  
  260. return result;
  261. };
  262.  
  263. const createSidebarList = (items) => {
  264. const sidebarItems = items.map((item) => {
  265. const href = item.uri || `/u/${item.id}-${item.name.toLowerCase()}`;
  266.  
  267. return createSidebarItem(item.name, item.avatar.data.uuid, href);
  268. });
  269.  
  270. const title = cn('div', { class: 'sidebar-sibs__title' }, ['Подписки:']);
  271.  
  272. const listWrapper = cn('div', { class: 'sidebar-sibs__list modal-window__content' }, sidebarItems);
  273.  
  274. return cn('div', { class: 'sidebar-subs' }, [title, listWrapper]);
  275. };
  276.  
  277. const getProfileUrl = async () => {
  278. const userButton = await getDomElementAsync('.user');
  279. userButton.click();
  280.  
  281. const profileMenuItem = await getDomElementAsync('.user-card');
  282. userButton.click();
  283.  
  284. return profileMenuItem.href;
  285. }
  286.  
  287. const getUserId = async () => {
  288. const profileUrl = await getProfileUrl();
  289. const userId = profileUrl.split('/u/')[1].split('-')[0];
  290.  
  291. return userId || null;
  292. };
  293.  
  294. const injectSubscriptions = async () => {
  295. const userId = await getUserId();
  296.  
  297. document.body.classList.add('dtf-subs-script-inited');
  298.  
  299. if (!userId) {
  300. return;
  301. }
  302.  
  303. const subs = await fetchSubs(userId);
  304. const list = createSidebarList(subs);
  305.  
  306. const sidebarButton = await getDomElementAsync('.sidebar-editor-button');
  307. sidebarButton.after(list);
  308. };
  309.  
  310. const runAutoExpandComments = () => {
  311. const expandComments = async () => {
  312. const isCommentsSlicePage = location.search.includes('comment=');
  313.  
  314. if (isCommentsSlicePage) {
  315. // На странице со срезом комментариев не запускаем автораскрытие общего списка.
  316. // Кнопка expandCommentsButton вместо раскрытия открывает страницу самого поста.
  317. // Из-за чего страница среза не отображается, а сразу происходит редирект.
  318. return;
  319. }
  320.  
  321. const expandCommentsButton = await getDomElementAsync('.comments-limit__expand');
  322. expandCommentsButton?.click();
  323. };
  324.  
  325. observeUrlChange(expandComments);
  326.  
  327. expandComments();
  328. };
  329.  
  330. const injectScrollUp = () => {
  331. const createScrollUpButton = () => {
  332. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  333. svg.setAttribute('class','icon');
  334. svg.setAttribute('width','20px');
  335. svg.setAttribute('height','20px');
  336.  
  337. const iconUp = document.createElementNS('http://www.w3.org/2000/svg', 'use');
  338. iconUp.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href','#chevron_up');
  339.  
  340. const iconDown = document.createElementNS('http://www.w3.org/2000/svg', 'use');
  341. iconDown.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href','#chevron_down');
  342.  
  343. svg.appendChild(iconUp);
  344. svg.appendChild(iconDown);
  345.  
  346. return cn('div', { class: 'scroll-up-button' }, [svg], document.querySelector('body'));
  347. };
  348.  
  349. const scrollUpBtn = createScrollUpButton();
  350.  
  351. const setUpIcon = () => {
  352. scrollUpBtn.classList.remove('down');
  353. scrollUpBtn.classList.add('up');
  354. };
  355.  
  356. const setDownIcon = () => {
  357. scrollUpBtn.classList.remove('up');
  358. scrollUpBtn.classList.add('down')
  359. };
  360.  
  361. let prevScrollPosition = 0;
  362.  
  363. window.addEventListener('scroll', debounce(() => {
  364. if (window.scrollY) {
  365. scrollUpBtn.classList.add('visible');
  366.  
  367. setUpIcon();
  368. } else {
  369. setDownIcon();
  370. }
  371. }, 100));
  372.  
  373. scrollUpBtn.addEventListener('click', () => {
  374. if (window.scrollY) {
  375. prevScrollPosition = window.scrollY;
  376.  
  377. setDownIcon();
  378.  
  379. window.scrollTo({
  380. top: 0,
  381. left: 0,
  382. behavior: "smooth",
  383. });
  384. } else {
  385. setUpIcon();
  386.  
  387. window.scrollTo({
  388. top: prevScrollPosition,
  389. left: 0,
  390. behavior: "smooth",
  391. });
  392. }
  393. });
  394. };
  395.  
  396. const init = async () => {
  397. injectStyles();
  398. injectSubscriptions();
  399.  
  400. if (ENABLE_AUTO_EXPAND_COMMENTS) {
  401. runAutoExpandComments();
  402. }
  403.  
  404. if (SHOW_SCROLL_UP_BUTTON) {
  405. injectScrollUp();
  406. }
  407. };
  408.  
  409.  
  410. init();
  411. })();