• 沧浪同学

    从 如果设计成这种视图帖子的阅读效率会更高 - 运营反馈 / 运营反馈, Lv1 - LINUX DO 继续讨论

    做了个 Linux.do 双栏阅读预览油猴脚本。

    点击帖子标题后,右侧打开帖子详情,左侧列表保持不跳转。支持拖动宽度、关闭、放大、刷新和原页打开,方便连续浏览帖子。

    目前还在细节调整中,欢迎反馈。

    宽窄可在中间手动拖动

    图片```
    // ==UserScript==
    // @name Linux.do 双栏阅读预览
    // @namespace https://linux.do/
    // @version 0.1.0
    // @description 在 Linux.do 话题列表中用右侧面板预览帖子
    // @match https://linux.do/*
    // @run-at document-start
    // @noframes
    // @grant none
    // ==/UserScript==

    (function () {
    'use strict';

    const PANEL_ID = 'ld-double-column-panel';
    const STYLE_ID = 'ld-double-column-style';
    const FRAME_STYLE_ID = 'ld-double-column-frame-style';
    const WIDTH_KEY = 'ld-double-column-width';
    const DEFAULT_WIDTH = 600;
    const MAX_WIDTH = 680;
    const MIN_WIDTH = 380;
    const MOBILE_BREAKPOINT = 780;

    const state = {
    panel: null,
    frame: null,
    title: null,
    status: null,
    currentUrl: '',
    open: false,
    fullscreen: false,
    width: readWidth(),
    savedScrollY: 0,
    parentUrl: '',
    historyActive: false,
    closeFromHistory: false,
    };

    function readWidth() {
    const value = Number.parseInt(localStorage.getItem(WIDTH_KEY), 10);
    return Number.isFinite(value) ? value : DEFAULT_WIDTH;
    }

    function clampWidth(value) {
    const max = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.floor(window.innerWidth * 0.6)));
    return Math.min(Math.max(value, MIN_WIDTH), max);
    }

    function normalizeTopicUrl(href) {
    try {
    const url = new URL(href, location.href);
    if (url.origin !== location.origin) return null;
    if (!/^\/t\/topic\/\d+(?:\/\d+)?$/.test(url.pathname)) return null;
    url.hash = '';
    return url.href;
    } catch {
    return null;
    }
    }

    function isModifiedClick(event) {
    return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
    }

    function addStyle() {
    if (!document.head || document.getElementById(STYLE_ID)) return;

    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
    :root {
    --ld-preview-width: ${state.width}px;
    }

    #${PANEL_ID} {
    position: fixed;
    z-index: 2147483000;
    inset: 0 0 0 auto;
    display: none;
    width: var(--ld-preview-width);
    min-width: 0;
    flex-direction: column;
    overflow: hidden;
    background: var(--secondary, #ffffff);
    color: var(--primary, #202124);
    border-left: 1px solid var(--primary-low, #d7d7d7);
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18);
    }

    body.ld-double-column-open #${PANEL_ID} {
    display: flex;
    }

    body.ld-double-column-open #main-outlet {
    width: calc(100% - var(--ld-preview-width)) !important;
    max-width: none !important;
    min-width: 0 !important;
    margin-right: var(--ld-preview-width) !important;
    box-sizing: border-box !important;
    }

    body.ld-double-column-open #main-outlet > .container,
    body.ld-double-column-open #main-outlet .topic-list,
    body.ld-double-column-open #main-outlet .topic-list-body {
    width: 100% !important;
    max-width: none !important;
    min-width: 0 !important;
    }

    body.ld-double-column-fullscreen #${PANEL_ID} {
    width: 100vw;
    }

    body.ld-double-column-fullscreen #main-outlet {
    visibility: hidden !important;
    }

    #${PANEL_ID} .ld-preview-resizer {
    position: absolute;
    z-index: 2;
    top: 0;
    bottom: 0;
    left: -5px;
    width: 10px;
    cursor: col-resize;
    touch-action: none;
    }

    #${PANEL_ID} .ld-preview-resizer::after {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 4px;
    width: 2px;
    content: '';
    background: transparent;
    transition: background 120ms ease;
    }

    #${PANEL_ID} .ld-preview-resizer:hover::after,
    body.ld-double-column-resizing #${PANEL_ID} .ld-preview-resizer::after {
    background: var(--tertiary, #0088cc);
    }

    #${PANEL_ID} .ld-preview-toolbar {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 42px;
    padding: 0 8px 0 14px;
    color: var(--primary, #202124);
    background: var(--secondary, #ffffff);
    border-bottom: 1px solid var(--primary-low, #d7d7d7);
    }

    #${PANEL_ID} .ld-preview-heading {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.3;
    text-overflow: ellipsis;
    white-space: nowrap;
    }

    #${PANEL_ID} .ld-preview-action {
    display: inline-grid;
    width: 30px;
    height: 30px;
    flex: 0 0 30px;
    place-items: center;
    padding: 0;
    color: inherit;
    background: transparent;
    border: 0;
    border-radius: 4px;
    cursor: pointer;
    font: inherit;
    font-size: 18px;
    line-height: 1;
    }

    #${PANEL_ID} .ld-preview-action:hover,
    #${PANEL_ID} .ld-preview-action:focus-visible {
    background: var(--primary-low, #e9e9e9);
    outline: none;
    }

    #${PANEL_ID} .ld-preview-content {
    position: relative;
    min-height: 0;
    flex: 1;
    background: var(--secondary, #ffffff);
    }

    #${PANEL_ID} .ld-preview-frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
    opacity: 1;
    transition: opacity 120ms ease;
    }

    #${PANEL_ID}.is-loading .ld-preview-frame {
    opacity: 0.35;
    }

    #${PANEL_ID} .ld-preview-status {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 1;
    max-width: min(80%, 360px);
    padding: 12px 16px;
    color: var(--primary, #202124);
    background: var(--secondary, #ffffff);
    border: 1px solid var(--primary-low, #d7d7d7);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
    font-size: 14px;
    line-height: 1.5;
    text-align: center;
    transform: translate(-50%, -50%);
    }

    #${PANEL_ID} .ld-preview-status[hidden] {
    display: none;
    }

    body.ld-double-column-resizing,
    body.ld-double-column-resizing * {
    cursor: col-resize !important;
    user-select: none !important;
    }

    @media (max-width: ${MOBILE_BREAKPOINT}px) {
    #${PANEL_ID} {
    width: 100vw;
    }

    body.ld-double-column-open #main-outlet {
    width: 100% !important;
    margin-right: 0 !important;
    }

    body.ld-double-column-open:not(.ld-double-column-fullscreen) #main-outlet {
    visibility: hidden !important;
    }

    #${PANEL_ID} .ld-preview-resizer {
    display: none;
    }
    }
    `;
    document.head.appendChild(style);
    }

    function createButton(icon, label, handler) {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'ld-preview-action';
    button.textContent = icon;
    button.title = label;
    button.setAttribute('aria-label', label);
    button.addEventListener('click', handler);
    return button;
    }

    function ensurePanel() {
    if (state.panel) return;
    addStyle();

    const panel = document.createElement('aside');
    panel.id = PANEL_ID;
    panel.setAttribute('aria-label', '帖子预览');

    const resizer = document.createElement('div');
    resizer.className = 'ld-preview-resizer';
    resizer.title = '拖动调整宽度';
    resizer.setAttribute('role', 'separator');
    resizer.setAttribute('aria-orientation', 'vertical');

    const toolbar = document.createElement('div');
    toolbar.className = 'ld-preview-toolbar';

    const heading = document.createElement('div');
    heading.className = 'ld-preview-heading';
    heading.textContent = '帖子预览';
    heading.title = '帖子预览';

    const content = document.createElement('div');
    content.className = 'ld-preview-content';

    const frame = document.createElement('iframe');
    frame.className = 'ld-preview-frame';
    frame.title = '帖子预览';
    frame.loading = 'eager';
    frame.referrerPolicy = 'origin-when-cross-origin';
    frame.addEventListener('load', handleFrameLoad);

    const status = document.createElement('div');
    status.className = 'ld-preview-status';
    status.hidden = true;

    const originalButton = createButton('↗', '在原页面打开', () => {
    if (state.currentUrl) window.open(state.currentUrl, '_blank', 'noopener,noreferrer');
    });
    const refreshButton = createButton('↻', '刷新预览', () => {
    if (state.frame?.contentWindow) state.frame.contentWindow.location.reload();
    });
    const fullscreenButton = createButton('⛶', '放大预览', () => {
    state.fullscreen = !state.fullscreen;
    document.body.classList.toggle('ld-double-column-fullscreen', state.fullscreen);
    fullscreenButton.textContent = state.fullscreen ? '⛶' : '⛶';
    fullscreenButton.title = state.fullscreen ? '恢复双栏' : '放大预览';
    fullscreenButton.setAttribute('aria-label', fullscreenButton.title);
    });
    const closeButton = createButton('×', '关闭预览', closePanel);

    toolbar.append(heading, originalButton, refreshButton, fullscreenButton, closeButton);
    content.append(frame, status);
    panel.append(resizer, toolbar, content);
    document.body.appendChild(panel);

    state.panel = panel;
    state.frame = frame;
    state.title = heading;
    state.status = status;

    installResizer(resizer);
    }

    function installResizer(resizer) {
    let pointerId = null;

    resizer.addEventListener('pointerdown', (event) => {
    if (!state.open || window.innerWidth <= MOBILE_BREAKPOINT) return;
    pointerId = event.pointerId;
    resizer.setPointerCapture(pointerId);
    document.body.classList.add('ld-double-column-resizing');
    event.preventDefault();
    });

    resizer.addEventListener('pointermove', (event) => {
    if (pointerId !== event.pointerId) return;
    setWidth(window.innerWidth - event.clientX);
    });

    const stopResize = (event) => {
    if (pointerId !== event.pointerId) return;
    pointerId = null;
    document.body.classList.remove('ld-double-column-resizing');
    try {
    resizer.releasePointerCapture(event.pointerId);
    } catch {
    // The pointer may already have been released by the browser.
    }
    };

    resizer.addEventListener('pointerup', stopResize);
    resizer.addEventListener('pointercancel', stopResize);
    }

    function setWidth(value) {
    state.width = clampWidth(value);
    document.documentElement.style.setProperty('--ld-preview-width', ${state.width}px);
    localStorage.setItem(WIDTH_KEY, String(state.width));
    }

    function setStatus(message) {
    if (!state.status) return;
    state.status.textContent = message;
    state.status.hidden = !message;
    }

    function setPreviewHistory(url, replace) {
    const base = new URL(state.parentUrl || location.href);
    base.hash = ld-preview=${encodeURIComponent(new URL(url).pathname)};
    const previousState = history.state && typeof history.state === 'object' ? history.state : {};
    const nextState = {
    ...previousState,
    ldDoubleColumn: true,
    ldPreviewUrl: url,
    ldPreviewBase: state.parentUrl,
    };

    if (replace) history.replaceState(nextState, '', base.href);
    else history.pushState(nextState, '', base.href);
    }

    function openPanel(url, options = {}) {
    const normalizedUrl = normalizeTopicUrl(url);
    if (!normalizedUrl) return;
    ensurePanel();

    if (!state.open) {
    state.open = true;
    state.fullscreen = false;
    state.savedScrollY = window.scrollY;
    state.parentUrl = location.href;
    state.historyActive = !options.fromHistory;
    document.body.classList.add('ld-double-column-open');
    document.body.classList.remove('ld-double-column-fullscreen');
    if (!options.fromHistory) setPreviewHistory(normalizedUrl, false);
    requestAnimationFrame(() => window.scrollTo(0, state.savedScrollY));
    } else if (!options.fromHistory && state.historyActive) {
    setPreviewHistory(normalizedUrl, true);
    }

    state.currentUrl = normalizedUrl;
    state.panel.classList.add('is-loading');
    state.title.textContent = '正在加载帖子...';
    state.title.title = normalizedUrl;
    setStatus('正在加载帖子...');
    state.frame.src = normalizedUrl;
    }

    function closePanel() {
    if (!state.open) return;
    const shouldGoBack = state.historyActive && history.state?.ldDoubleColumn && !state.closeFromHistory;
    state.closeFromHistory = false;
    state.open = false;
    state.fullscreen = false;
    state.currentUrl = '';
    document.body.classList.remove('ld-double-column-open', 'ld-double-column-fullscreen');
    state.panel.classList.remove('is-loading');
    state.frame.removeAttribute('src');
    setStatus('');
    state.title.textContent = '帖子预览';
    state.title.title = '帖子预览';
    requestAnimationFrame(() => window.scrollTo(0, state.savedScrollY));

    if (shouldGoBack) {
    state.historyActive = false;
    history.back();
    }
    }

    function handleFrameLoad() {
    state.panel?.classList.remove('is-loading');
    setStatus('');

    try {
    const frameDocument = state.frame.contentDocument;
    if (!frameDocument) throw new Error('preview document unavailable');

    let style = frameDocument.getElementById(FRAME_STYLE_ID);
    if (!style) {
    style = frameDocument.createElement('style');
    style.id = FRAME_STYLE_ID;
    style.textContent = `
    html, body { min-width: 0 !important; }
    .d-header,
    .sidebar-container,
    .sidebar-wrapper,
    .sidebar-hamburger,
    .before-header-panel-outlet,
    .topic-list { display: none !important; }
    #main-outlet,
    #topic,
    .topic-area,
    .topic-post,
    .boxed { width: auto !important; max-width: none !important; margin-left: 0 !important; margin-right: 0 !important; }
    #main-outlet { padding: 14px 20px 30px !important; }
    .topic-post { border-left: 0 !important; border-right: 0 !important; }
    `;
    frameDocument.head.appendChild(style);
    }

    const topicTitle = frameDocument.querySelector('h1')?.textContent?.replace(/\s+/g, ' ').trim();
    if (topicTitle) {
    state.title.textContent = topicTitle;
    state.title.title = topicTitle;
    } else {
    state.title.textContent = '帖子预览';
    state.title.title = state.currentUrl;
    }
    } catch {
    state.title.textContent = '帖子预览';
    state.title.title = state.currentUrl;
    }
    }

    function handleTopicClick(event) {
    if (isModifiedClick(event) || event.defaultPrevented) return;
    const rawTarget = event.target;
    const target = rawTarget && typeof rawTarget.closest === 'function'
    ? rawTarget.closest('a[href]')
    : null;
    if (!target || target.closest(#${PANEL_ID})) return;
    if (!target.matches('a.title.raw-topic-link') && !target.closest('tr.topic-list-item')) return;

    const topicUrl = normalizeTopicUrl(target.href);
    if (!topicUrl) return;

    event.preventDefault();
    event.stopImmediatePropagation();
    openPanel(topicUrl);
    }

    function handleHistoryChange() {
    const historyState = history.state;
    if (historyState?.ldDoubleColumn && historyState.ldPreviewUrl) {
    openPanel(historyState.ldPreviewUrl, { fromHistory: true });
    return;
    }

    if (state.open) {
    state.closeFromHistory = true;
    closePanel();
    }
    }

    function start() {
    addStyle();
    window.addEventListener('click', handleTopicClick, true);
    window.addEventListener('popstate', handleHistoryChange);
    window.addEventListener('keydown', (event) => {
    if (event.key === 'Escape' && state.open) closePanel();
    });
    setWidth(state.width);
    }

    if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
    } else {
    start();
    }
    })();

    主楼
  • X_DO

    已经用上了,挺方便的,总算不用跳来跳去或者鼠标移动上去点击了。

    不过目前对宽屏显示器不太友好,我自己用 codex 优化下

    #1
  • X_DO

    在佬的基础上做了一版简化,没有使用 iframe 替换,改成 json 渲染。

    https://raw.githubusercontent.com/ppanphper/linuxdo-double-column/main/linuxdo-double-column.user.jslinux 双栏预览脚本

    #2
  • wadax

    能不能整一个实时显示最新帖子的油猴脚本

    #3
  • 火车嘟嘟

    good 实测 好用 真不错 真不错

    #4

发表回复

登录后回复