- // ==UserScript==
- // @name DTF Enhancer
- // @namespace http://tampermonkey.net/
- // @version 0.1.8
- // @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 = '1400px'; // Ширина контентной области. Можно задать любое значение, например: 960px или 1200px
- const SHOW_SCROLL_UP_BUTTON = 1; // Показывать кнопку "вверх". 0 - выкл, 1 - вкл
- const ENABLE_AUTO_EXPAND_COMMENTS = 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) => typeof dict[char] === 'undefined' ? char : dict[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, debugMessage = '') => {
- return new Promise((resolve, reject) => {
- try {
- let timerId;
-
- setTimeout(() => {
- if (timerId) {
- console.debug(`Время ожидания DOM элемента ${selector} истекло (${timerLimit / 1000}s)`);
- resolve(null);
- clearTimeout(timerId);
- }
- }, timerLimit);
-
- 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 = `
- :root {
- ${USE_WIDE_LAYOUT ? '--layout-max-width: none;' : '--layout-max-width: ' + CONTENT_WIDTH}
- }
-
- .sidebar-subs {
- display: flex;
- flex-direction: column;
- overflow: auto;
- margin: 24px 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;
-
- ${SHOW_SCROLL_UP_BUTTON ? 'padding: 0 40px;' : ''}
- }
-
- .header__left,
- .aside--left {
- width: 220px;
- flex-shrink: 0;
- }
-
- .header__right,
- .aside--right {
- width: 300px;
- flex-shrink: 0;
- }
-
- .header__right {
- justify-content: flex-end;
- margin-left: 0;
- }
-
- .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 || `/id${item.id}`;
-
- return createSidebarItem(item.name, item.avatar.data.uuid, href);
- });
-
- const title = cn('div', { class: 'sidebar__title' }, ['Подписки:']);
-
- const listWrapper = cn('div', { class: 'sidebar__subs' }, sidebarItems);
-
- return cn('div', { class: 'sidebar-subs' }, [title, listWrapper]);
- };
-
- const getProfileUrl = async () => {
- const userButton = await getDomElementAsync('.account-button__inner');
- userButton.click();
-
- const profileMenuItem = await getDomElementAsync('.user-card');
- userButton.click();
-
- return profileMenuItem.href;
- }
-
- const getUserId = async () => {
- const profileUrl = await getProfileUrl();
- const userId = profileUrl.split('/id')[1];
-
- 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 firstSidebarSection = await getDomElementAsync('.sidebar__section');
- firstSidebarSection.after(list);
- };
-
- const runAutoExpandComments = () => {
- const expandComments = async () => {
- const isCommentsSlicePage = location.search.includes('comment=');
-
- if (isCommentsSlicePage) {
- // На странице со срезом комментариев не запускаем автораскрытие общего списка.
- // Кнопка expandCommentsButton вместо раскрытия открывает страницу самого поста.
- // Из-за чего страница среза не отображается, а сразу происходит редирект.
- return;
- }
-
- 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 start = async () => {
- console.debug('DTF Enhancer started');
-
- injectStyles();
- injectSubscriptions();
-
- if (ENABLE_AUTO_EXPAND_COMMENTS) {
- runAutoExpandComments();
- }
-
- if (SHOW_SCROLL_UP_BUTTON) {
- injectScrollUp();
- }
- };
-
- const init = async () => {
- if (document.visibilityState === 'visible') {
- start();
- } else {
- // Для вкладок открытых в фоне запускаем скрипт после перехода на вкладку
- addEventListener("visibilitychange", (event) => {
- if (document.visibilityState === 'visible') {
- start();
- }
- }, { once: true });
- }
- };
-
- init();
- })();