您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
80vh columns, width from longest line, per-column ▲▼ controls, per-URL save; global "Show Columns" toggle.
// ==UserScript== // @name Freetar: Adjustable columns + global Show Columns toggle // @namespace https://example.com/ // @version 3.1 // @description 80vh columns, width from longest line, per-column ▲▼ controls, per-URL save; global "Show Columns" toggle. // @match http://localhost:22000/* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; const SELECTOR = 'div.tab.font-monospace'; const STORAGE_KEY_PREFIX = 'ftColumns:'; const GLOBAL_ENABLE_KEY = 'ftEnabled'; const SAVE_DEBOUNCE = 150; const EXTRA_CH = 3; // extra width beyond the longest line const TOGGLE_ID = 'checkbox_show_columns'; let enabled = GM_getValue(GLOBAL_ENABLE_KEY, true); let saveTimer = null; let moEnhancer = null; let moToolbar = null; let enhanced = false; let originalHTML = null; let tabEl = null; let onResizeHandler = null; function storageKeyForPage() { return STORAGE_KEY_PREFIX + location.pathname; } function saveCounts(root) { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { const counts = Array.from(root.querySelectorAll('.ft-col-body')) .map(b => b.children.length); try { GM_setValue(storageKeyForPage(), JSON.stringify({ counts, v: 2 })); } catch {} }, SAVE_DEBOUNCE); } function loadCounts() { try { const raw = GM_getValue(storageKeyForPage()); if (!raw) return null; const parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.counts)) return parsed.counts; return null; } catch { return null; } } // Convert original tab content into div.ft-line elements (one per visual line) function extractLines(container) { const lines = []; let buffer = []; const flush = () => { const el = document.createElement('div'); el.className = 'ft-line'; if (buffer.length === 0) { el.innerHTML = ' '; // keep blank line } else { buffer.forEach(n => el.appendChild(n)); } lines.push(el); buffer = []; }; Array.from(container.childNodes).forEach(node => { if (node.nodeName === 'BR') { flush(); } else { buffer.push(node.cloneNode(true)); } }); if (buffer.length) flush(); return lines; } function computeLongestLineCh(lines) { let maxLen = 0; for (const line of lines) { const t = line.textContent.replace(/\u00A0/g, ' '); if (t.length > maxLen) maxLen = t.length; } return maxLen + EXTRA_CH; } function injectStyles() { if (document.querySelector('style[data-ft-enhanced-style="1"]')) return; const style = document.createElement('style'); style.setAttribute('data-ft-enhanced-style', '1'); style.textContent = ` :root { color-scheme: light dark; } /* Make Bootstrap container full-width for more room */ .container { max-width: 100% !important; width: 100% !important; } ${SELECTOR}.ft-enhanced { display: flex; align-items: stretch; gap: 2rem; height: 80vh; max-height: 80vh; overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; scrollbar-gutter: stable both-edges; padding: 10px; font-variant-ligatures: none; } ${SELECTOR}.ft-enhanced .ft-col { flex: 0 0 auto; width: var(--ft-col-width, 60ch); min-width: var(--ft-col-width, 60ch); max-width: var(--ft-col-width, 60ch); display: flex; flex-direction: column; position: relative; padding: 0 10px; /* internal padding so columns don't crowd */ border-right: 1px solid rgba(128,128,128,0.35); /* vertical rule */ } ${SELECTOR}.ft-enhanced .ft-col:last-child { border-right: none; } ${SELECTOR}.ft-enhanced .ft-col-body { flex: 1 1 auto; overflow: hidden; /* no vertical scroll; we push lines to next col */ line-height: 1.45; } ${SELECTOR}.ft-enhanced .ft-line { white-space: pre; /* do not wrap; width is set from longest line */ padding-block: 2px; } ${SELECTOR}.ft-enhanced .ft-col-controls { flex: 0 0 auto; display: flex; gap: 6px; justify-content: center; padding: 6px 0 4px; opacity: 0.8; } ${SELECTOR}.ft-enhanced .ft-btn { width: 24px; height: 24px; border-radius: 999px; border: 1px solid color-mix(in oklab, currentColor 25%, transparent); background: color-mix(in oklab, currentColor 5%, transparent); color: inherit; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; cursor: pointer; opacity: 0.55; transition: opacity .15s ease, background-color .15s ease; user-select: none; } ${SELECTOR}.ft-enhanced .ft-btn:hover { opacity: 0.95; background: color-mix(in oklab, currentColor 10%, transparent); } /* Small screens: fall back to a single flowing block */ @media (max-width: 768px) { ${SELECTOR}.ft-enhanced { height: auto; max-height: none; overflow: visible; display: block; padding: 10px 0; } ${SELECTOR}.ft-enhanced .ft-col { width: auto; min-width: 0; max-width: none; border-right: none; padding: 0; } ${SELECTOR}.ft-enhanced .ft-col-body { overflow: visible; } ${SELECTOR}.ft-enhanced .ft-col-controls { justify-content: flex-start; padding-bottom: 0.75rem; } } `; document.head.appendChild(style); } function removeStyles() { const el = document.querySelector('style[data-ft-enhanced-style="1"]'); if (el) el.remove(); } function createColumn() { const col = document.createElement('div'); col.className = 'ft-col'; const body = document.createElement('div'); body.className = 'ft-col-body'; const controls = document.createElement('div'); controls.className = 'ft-col-controls'; const btnUp = document.createElement('button'); btnUp.className = 'ft-btn'; btnUp.title = 'Pull one line from the next column'; btnUp.textContent = '▲'; const btnDown = document.createElement('button'); btnDown.className = 'ft-btn'; btnDown.title = 'Push one line to the next column'; btnDown.textContent = '▼'; controls.append(btnUp, btnDown); col.append(body, controls); return { col, body, btnUp, btnDown }; } function getColumns(root) { return Array.from(root.querySelectorAll('.ft-col')); } function getBodies(root) { return Array.from(root.querySelectorAll('.ft-col-body')); } function ensureNoOverflow(root) { let bodies = getBodies(root); for (let i = 0; i < bodies.length; i++) { const body = bodies[i]; while (body.scrollHeight > body.clientHeight && body.lastElementChild) { const next = bodies[i + 1] || createColumnAndAttach(root).body; bodies = getBodies(root); const current = bodies[i]; const nextBody = bodies[i + 1]; nextBody.prepend(current.lastElementChild); } } // Remove trailing empty columns (keep at least one) const cols = getColumns(root); for (let i = cols.length - 1; i > 0; i--) { const b = cols[i].querySelector('.ft-col-body'); if (b && b.children.length === 0) cols[i].remove(); else break; } } function createColumnAndAttach(root) { const parts = createColumn(); root.appendChild(parts.col); attachHandlers(root, parts); return parts; } function attachHandlers(root, parts) { parts.btnDown.addEventListener('click', () => { const cols = getColumns(root); const idx = cols.indexOf(parts.col); const bodies = getBodies(root); const body = bodies[idx]; if (!body || !body.lastElementChild) return; const next = bodies[idx + 1] || createColumnAndAttach(root).body; next.prepend(body.lastElementChild); ensureNoOverflow(root); saveCounts(root); }); parts.btnUp.addEventListener('click', () => { const cols = getColumns(root); const idx = cols.indexOf(parts.col); const bodies = getBodies(root); const body = bodies[idx]; const next = bodies[idx + 1]; if (!body || !next || !next.firstElementChild) return; const candidate = next.firstElementChild; body.append(candidate); if (body.scrollHeight > body.clientHeight) { next.prepend(body.lastElementChild); return; } ensureNoOverflow(root); saveCounts(root); }); } function buildAuto(root, lines) { let c = createColumnAndAttach(root); for (let i = 0; i < lines.length; i++) { c.body.append(lines[i]); if (c.body.scrollHeight > c.body.clientHeight) { const next = createColumnAndAttach(root); next.body.prepend(c.body.lastElementChild); c = next; } } ensureNoOverflow(root); } function buildFromCounts(root, lines, counts) { let i = 0; for (let colIdx = 0; colIdx < counts.length; colIdx++) { const { body } = createColumnAndAttach(root); for (let put = 0; put < counts[colIdx] && i < lines.length; put++) { body.append(lines[i++]); } } while (i < lines.length) { const { body } = createColumnAndAttach(root); while (i < lines.length) { body.append(lines[i++]); if (body.scrollHeight > body.clientHeight) { const next = createColumnAndAttach(root); next.body.prepend(body.lastElementChild); break; } } } ensureNoOverflow(root); } function enhance() { if (enhanced) return; const tab = document.querySelector(SELECTOR); if (!tab) return; // Save original content for restoration tabEl = tab; originalHTML = tabEl.innerHTML; injectStyles(); // Build columns from lines const lines = extractLines(tabEl); const longestCh = computeLongestLineCh(lines); tabEl.classList.add('ft-enhanced'); tabEl.style.setProperty('--ft-col-width', `${longestCh}ch`); tabEl.innerHTML = ''; const saved = loadCounts(); if (saved && saved.length) { buildFromCounts(tabEl, lines, saved); } else { buildAuto(tabEl, lines); saveCounts(tabEl); } // keep columns safe after layout settles setTimeout(() => { ensureNoOverflow(tabEl); saveCounts(tabEl); }, 0); onResizeHandler = () => { ensureNoOverflow(tabEl); saveCounts(tabEl); }; window.addEventListener('resize', onResizeHandler, { passive: true }); // If page mutates, re-ensure (but do not re-extract/replace) if (!moEnhancer) { moEnhancer = new MutationObserver(() => { if (!enabled) return; ensureNoOverflow(tabEl); }); moEnhancer.observe(document.documentElement, { childList: true, subtree: true }); } enhanced = true; } function disableAndRestore() { if (!enhanced) return; // Remove resize handler if (onResizeHandler) { window.removeEventListener('resize', onResizeHandler); onResizeHandler = null; } // Disconnect enhancer observer if (moEnhancer) { moEnhancer.disconnect(); moEnhancer = null; } // Restore original content if (tabEl) { tabEl.classList.remove('ft-enhanced'); tabEl.removeAttribute('style'); tabEl.innerHTML = originalHTML || tabEl.innerHTML; } // Remove injected styles so page returns to original layout removeStyles(); enhanced = false; } // Insert the "Show Columns" toggle (global) function insertToggle() { if (document.getElementById(TOGGLE_ID)) return; // Find the block that contains transpose controls const transposeDown = document.getElementById('transpose_down'); if (!transposeDown) return; const transposeBlock = transposeDown.closest('div'); const toolbar = transposeBlock?.parentElement; if (!toolbar) return; // Create switch styled like "Show chords" const wrap = document.createElement('div'); wrap.className = 'form-check form-switch me-4'; const input = document.createElement('input'); input.className = 'form-check-input'; input.type = 'checkbox'; input.role = 'switch'; input.id = TOGGLE_ID; input.checked = !!enabled; const label = document.createElement('label'); label.className = 'form-check-label'; label.setAttribute('for', TOGGLE_ID); label.textContent = 'Show Columns'; wrap.append(input, label); // Insert right after the transpose block if (transposeBlock.nextSibling) { toolbar.insertBefore(wrap, transposeBlock.nextSibling); } else { toolbar.appendChild(wrap); } input.addEventListener('change', () => { enabled = input.checked; GM_setValue(GLOBAL_ENABLE_KEY, enabled); if (enabled) { // If previously disabled, re-enable columns enhance(); } else { // Turn off and restore original layout disableAndRestore(); } }); } function ensureToggleInserted() { insertToggle(); if (!moToolbar) { moToolbar = new MutationObserver(() => insertToggle()); moToolbar.observe(document.documentElement, { childList: true, subtree: true }); } } function init() { ensureToggleInserted(); if (enabled) { enhance(); } else { disableAndRestore(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();