DTF Enhancer

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

目前為 2024-03-19 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         DTF Enhancer
// @namespace    http://tampermonkey.net/
// @version      0.0.9
// @description  Выводит список подписок в сайдбаре и раскрывает список комментов
// @author       You
// @match        *://dtf.ru/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dtf.ru
// @run-at       document-end
// @grant        GM.getValue
// @grant        GM.setValue
// @license      MIT
// ==/UserScript==

(function() {
  const USE_WIDE_LAYOUT = 0; // Показывать страницу на всю ширину. 0 - выкл, 1 - вкл
  const CONTENT_WIDTH = '800px'; // Ширина контентной области. Можно задать любое значение, например: 960px или 1200px
  const SHOW_SCROLL_UP_BUTTON = 1; // Показывать кнопку "вверх". 0 - выкл, 1 - вкл

  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"};

  const transliterate = (word) => word.split('').map((char) => dict[char] || char).join("");

  const cn = (tagName, attrs = {}, childrenList = [], parentNode = null) => {
    const node = document.createElement(tagName);

    if (typeof attrs === 'object') {
        for (const attrsKey in attrs) node.setAttribute(attrsKey, attrs[attrsKey]);
    }

    if (Array.isArray(childrenList)) {
        childrenList.forEach(child => {
            node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
        });
    }

    if (parentNode) {
        parentNode.appendChild(node);
    }

    return node;
  };

  const getDomElementAsync = (selector, timerLimit = 10000) => {
    return new Promise((resolve, reject) => {
        try {
            setTimeout(() => {
                console.log(`Время ожидания DOM элемента ${selector} истекло (${timerLimit / 1000}s)`);
                resolve(null);
            }, timerLimit);

            let timerId;

            const tick = () => {
                const element = document.querySelector(selector);

                if (element) {
                    clearTimeout(timerId);
                    resolve(element);
                } else {
                    timerId = setTimeout(tick, 100);
                }
            };

            tick();
        } catch (e) {
            reject(e);
        }
    });
};

  const debounce = (func, wait) => {
    let timeout;
    return function (...args) {
      return new Promise(resolve => {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          timeout = null;
          Promise.resolve(func.apply(this, [...args])).then(resolve);
        }, wait);
      });
    };
  };

  const observeUrlChange = async (onChange) => {
    await GM.setValue('currentUrl', window.location.href);

    const onChangeHandler = async () => {
      const oldHref = await GM.getValue('currentUrl');
      const newHref = window.location.href;

      if (oldHref !== newHref) {
        console.log('observeUrlChange');

        await GM.setValue('currentUrl', newHref);
        onChange?.();
      }
    };

    const debouncedOnChangeHandler = debounce(onChangeHandler, 500);

    const observer = new MutationObserver(debouncedOnChangeHandler);

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  };

  const injectStyles = () => {
    const styles = `
      .sidebar-subs {
        display: flex;
        flex-direction: column;
        overflow: auto;
        margin: 24px 0;
      }

      .sidebar-sibs__title {
        margin-bottom: 16px;
        padding: 0 8px;
        font-weight: 500;
        font-size: 18px;
      }

      .sidebar-sibs__list {
        padding: 0;
      }

      .sidebar-item._sub img.icon {
        width: 24px;
        border-radius: 50%;
      }

      .sidebar-item._sub span {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        min-width: 1px;
      }

      /* перебиваем стили DTF */
      .layout,
      .header__layout {
        display: flex;
        gap: 24px;
        ${USE_WIDE_LAYOUT ? 'max-width: none;' : ''}
        ${SHOW_SCROLL_UP_BUTTON ? 'padding: 0 40px;' : ''}
      }

      .header__left,
      .header__right,
      .aside {
        width: 220px;
        flex-shrink: 0;
      }

      .header__right {
        justify-content: flex-end;
        margin-left: 0;
      }

      .view,
      .header__search {
        width: 100%;
        max-width: ${CONTENT_WIDTH};
      }

      .sidebar__main {
        display: flex;
        flex-direction: column;
        flex-shrink: 0;
        min-width: 1px;
        overflow: auto;
      }

      .sidebar-item {
        flex-shrink: 0;
      }

      .sidebar-editor-button {
        margin-top: 24px;
      }

      .sidebar-editor-buttons {
        margin-top: auto;
        margin-bottom: 16px;
      }

      .account-menu {
        visibility: hidden;
      }

      body.dtf-subs-script-inited .account-menu {
        visibility: visible;
      }

      /* кнопка "вверх" */
      .scroll-up-button {
        display: flex;
        justify-content: center;
        position: fixed;
        top: var(--layout-header-height);;
        left: 0;
        width: 40px;
        height: 100vh;
        padding-top: 10px;
        background-color: transparent;
        cursor: pointer;
        z-index: var(--layout-z-index-header);
        opacity: 0;
        pointer-events: none;
        transition: background-color 200ms ease-out, opacity 200ms ease-out;
      }

      [data-theme="light"] .scroll-up-button:hover {
        background-color: var(--theme-color-brand-header);
      }

      [data-theme="dark"] .scroll-up-button:hover {
        background-color: rgba(255, 255, 255, .1);
      }

      .scroll-up-button use {
        display: none;
      }

      .scroll-up-button.up use:nth-child(1) {
        display: block;
      }

      .scroll-up-button.down use:nth-child(2) {
        display: block;
      }

      .scroll-up-button.visible {
        opacity: 1;
        pointer-events: all;
      }
    `;

    document.head.insertAdjacentHTML("beforeend", `<style type="text/css" id="dtfSubsStyles">${styles}</style>`)
  };

  const fetchSubs = async (userId) => {
    const resp = await fetch(`https://api.dtf.ru/v2.5/subsite/subscriptions?subsiteId=${userId}`);
    const { result } = await resp.json();

    return result.items;
  }

  const getImageUrl = (uuid) => `https://leonardo.osnova.io/${uuid}/-/scale_crop/32x32/`;

  const createSidebarItem = (name, imageId, href) => {
    const imgEl = cn('img', { class: 'icon', src: getImageUrl(imageId) });
    const nameEl = cn('span', {}, [name]);
    const result = cn('a', { class: 'sidebar-item _sub', href: transliterate(href), alt: name }, [imgEl, nameEl]);

    return result;
  };

  const createSidebarList = (items) => {
    const sidebarItems = items.map((item) => {
      const href = item.uri || `/u/${item.id}-${item.name.toLowerCase()}`;

      return createSidebarItem(item.name, item.avatar.data.uuid, href);
    });

    const title = cn('div', { class: 'sidebar-sibs__title' }, ['Подписки:']);

    const listWrapper = cn('div', { class: 'sidebar-sibs__list modal-window__content' }, sidebarItems);

    return cn('div', { class: 'sidebar-subs' }, [title, listWrapper]);
  };

  const getProfileUrl = async () => {
    const userButton = await getDomElementAsync('.user');
    userButton.click();

    const profileMenuItem = await getDomElementAsync('.account-menu__user-card');
    userButton.click();

    return profileMenuItem.href;
  }

  const getUserId = async () => {
    const profileUrl = await getProfileUrl();
    const userId = profileUrl.split('/u/')[1].split('-')[0];

    return userId || null;
  };

  const injectSubscriptions = async () => {
    const userId = await getUserId();

    document.body.classList.add('dtf-subs-script-inited');

    if (!userId) {
      return;
    }

    const subs = await fetchSubs(userId);
    const list = createSidebarList(subs);

    const sidebarButton = await getDomElementAsync('.sidebar-editor-button');
    sidebarButton.after(list);
  };

  const runAutoExpandComments = () => {
    const expandComments = async () => {
      const expandCommentsButton = await getDomElementAsync('.comments-limit__expand');
      expandCommentsButton?.click();
    };

    observeUrlChange(expandComments);

    expandComments();
  };

  const injectScrollUp = () => {
    const createScrollUpButton = () => {
      const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svg.setAttribute('class','icon');
            svg.setAttribute('width','20px');
            svg.setAttribute('height','20px');

      const iconUp = document.createElementNS('http://www.w3.org/2000/svg', 'use');
            iconUp.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href','#chevron_up');

      const iconDown = document.createElementNS('http://www.w3.org/2000/svg', 'use');
            iconDown.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href','#chevron_down');

      svg.appendChild(iconUp);
      svg.appendChild(iconDown);

      return cn('div', { class: 'scroll-up-button' }, [svg], document.querySelector('body'));
    };

    const scrollUpBtn = createScrollUpButton();

    const setUpIcon = () => {
      scrollUpBtn.classList.remove('down');
      scrollUpBtn.classList.add('up');
    };

    const setDownIcon = () => {
      scrollUpBtn.classList.remove('up');
      scrollUpBtn.classList.add('down')
    };

    let prevScrollPosition = 0;

    window.addEventListener('scroll', debounce(() => {
      if (window.scrollY) {
        scrollUpBtn.classList.add('visible');

        setUpIcon();
      } else {
        setDownIcon();
      }
    }, 100));

    scrollUpBtn.addEventListener('click', () => {
      if (window.scrollY) {
        prevScrollPosition = window.scrollY;

        setDownIcon();

        window.scrollTo({
          top: 0,
          left: 0,
          behavior: "smooth",
        });
      } else {
        setUpIcon();

        window.scrollTo({
          top: prevScrollPosition,
          left: 0,
          behavior: "smooth",
        });
      }
    });
  };

  const init = async () => {
    injectStyles();
    injectSubscriptions();
    runAutoExpandComments();

    if (SHOW_SCROLL_UP_BUTTON) {
      injectScrollUp();
    }
  };


  init();
})();