您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Выводит список подписок в сайдбаре и раскрывает список комментов
// ==UserScript== // @name DTF Enhancer // @namespace http://tampermonkey.net/ // @version 0.1.9 // @description Выводит список подписок в сайдбаре и раскрывает список комментов // @author undfndusr // @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__section._subs { margin: 24px 0; overflow: hidden; } .sidebar-item._sub img.icon { width: 28px; 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 title = cn('div', { class: 'sidebar__title' }, ['Подписки']); const sidebarItems = items.map((item) => createSidebarItem(item.name, item.avatar.data.uuid, item.uri || `/id${item.id}`)); return cn('div', { class: 'sidebar__section _subs' }, [title, ...sidebarItems]); }; 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(); })();