DTF Enhancer

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

As of 2024-03-14. See the latest version.

// ==UserScript==
// @name         DTF Enhancer
// @namespace    http://tampermonkey.net/
// @version      0.0.3
// @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 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 */
      .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;
      }
    `;

    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: href, alt: name }, [imgEl, nameEl]);

    return result;
  };

  const createSidebarList = (items) => {
    const sidebarItems = items.map((item) => {
      return createSidebarItem(item.name, item.avatar.data.uuid, item.uri);
    });

    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 init = async () => {
    injectStyles();
    injectSubscriptions();
    runAutoExpandComments();
  };


  init();
})();