// ==UserScript== // @name Cifra Club Desbloqueado // @name:en Cifra Club Unlocked // @namespace https://github.com/gkp1 // @version 2.0.0 // @description Cifra inteira (acordes + letra) numa tela só, em colunas com fonte automática, dividir em colunas e pedal passador sem anúncio, e botão de tema claro/escuro. // @description:en Whole song (chords + lyrics) on one screen with auto-sized columns, split columns and the page-turner pedal without the ad popup, and a light/dark switch. // @author gkp1 // @homepageURL https://github.com/gkp1/cifra-club-desbloqueado-userscript-extensao // @supportURL https://github.com/gkp1/cifra-club-desbloqueado-userscript-extensao/issues // @downloadURL https://raw.githubusercontent.com/gkp1/cifra-club-desbloqueado-userscript-extensao/main/cifraclub-desbloqueado.user.js // @updateURL https://raw.githubusercontent.com/gkp1/cifra-club-desbloqueado-userscript-extensao/main/cifraclub-desbloqueado.user.js // @license MIT // @match https://www.cifraclub.com.br/* // @match https://www.cifraclub.com/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; // ===== Config ===== const KEY_VIEW = 'l'; // liga/desliga o modo tela cheia const KEY_BROWSER_FS = 'f'; // tela cheia do navegador (dentro do modo) const KEY_THEME = 'd'; // tema claro/escuro (site e modo tela cheia) const START_ON = false; // abrir já no modo tela cheia? (depois lembra a última escolha) const UNLOCK = true; // libera "Dividir em colunas" e "Pedal passador" sem anúncio // ================== const ID = 'ccfit'; const STORE = 'ccfit-on'; let enabled = readPref(); let manualScale = 1; let overlay, cols, titleEl, lockStyle, dock, toggleBtn, themeBtns, banner, srcPre, observer, raf; let lastHtml = ''; let shownDark = null; const log = (...a) => console.log('%c[ccfit]', 'color:#ff8a3d;font-weight:bold', ...a); log('userscript loaded', location.href); function readPref() { try { const v = localStorage.getItem(STORE); return v === null ? START_ON : v === '1'; } catch (e) { return START_ON; } } function savePref(on) { try { localStorage.setItem(STORE, on ? '1' : '0'); } catch (e) {} } const DAY = 864e5; function getCookie(name) { const m = document.cookie.match(new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]*)`)); if (!m) return null; try { return decodeURIComponent(m[1]); } catch (e) { return m[1]; } } function setCookie(name, value, expires) { document.cookie = `${name}=${encodeURIComponent(value)};expires=${new Date(expires).toUTCString()};path=/`; } // ---------- Unlock ---------- // After you watch the ad, the site saves a 24h pass in a cookie. We write that same // cookie ourselves, so the ad popup never shows. Nothing is sent anywhere. const COLUMNS_PASS = 'cifraclub:fullscreen-rewarded-unlock'; const PEDAL_PASS = '_ccpt'; // Same hash (cyrb53) the site uses to sign the pedal pass. function hash(s) { let h1 = 0xdeadbeef, h2 = 0x41c6ce57; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); h1 = Math.imul(h1 ^ c, 0x9e3779b1); h2 = Math.imul(h2 ^ c, 0x5f356495); } h1 = Math.imul(h1 ^ (h1 >>> 16), 0x85ebca6b) ^ Math.imul(h2 ^ (h2 >>> 13), 0xc2b2ae35); h2 = Math.imul(h2 ^ (h2 >>> 16), 0x85ebca6b) ^ Math.imul(h1 ^ (h1 >>> 13), 0xc2b2ae35); return (0x100000000 * (0x1fffff & h2) + (h1 >>> 0)).toString(36); } function unlock() { if (getCookie(COLUMNS_PASS) !== 'true') { setCookie(COLUMNS_PASS, 'true', Date.now() + DAY); log('split columns unlocked'); } // The pedal pass is tied to your login, so it only applies when you're signed in. const jwt = getCookie('jwt'); if (!jwt) return; const [t, sig] = (getCookie(PEDAL_PASS) || '').split('.'); const exp = parseInt(t, 36); if (sig && exp > Date.now() && sig === hash(`${jwt}:${exp}:pt`)) return; const next = Date.now() + DAY; setCookie(PEDAL_PASS, `${next.toString(36)}.${hash(`${jwt}:${next}:pt`)}`, next); log('page-turner pedal unlocked'); } if (UNLOCK) unlock(); // ---------- UI text (follows your browser language) ---------- const L = /^pt\b/i.test(navigator.language || '') ? { exit: 'sair', fullscreen: 'tela cheia', size: 'tamanho', theme: 'tema', smaller: 'Menor', auto: 'Ajuste automático', bigger: 'Maior', back: 'Voltar ao site', toDark: 'Tema escuro', toLight: 'Tema claro', open: 'Tela cheia', press: 'Aperte', banner: 'para ver a cifra inteira em tela cheia', } : { exit: 'exit', fullscreen: 'fullscreen', size: 'size', theme: 'theme', smaller: 'Smaller', auto: 'Auto fit', bigger: 'Bigger', back: 'Back to the site', toDark: 'Dark theme', toLight: 'Light theme', open: 'Full screen', press: 'Press', banner: 'to see the whole song on one screen', }; const K = KEY_VIEW.toUpperCase(); const KF = KEY_BROWSER_FS.toUpperCase(); const KT = KEY_THEME.toUpperCase(); const ICON = { moon: '', sun: '', }; // All CSS lives inside our own elements, so React/Next hydration can't wipe it. // Colors follow the site's theme (data-theme on ), falling back to the system. const SCOPE = `#${ID}, #${ID}-banner, #${ID}-dock`; const DARK = ` --bg: #14161a; --panel: rgba(30,33,39,.8); --fg: #e9e6e1; --soft: #9a958d; --chord: #ff9a4d; --chord-bg: rgba(255,154,77,.10); --line: rgba(255,255,255,.08); --sec: #b197fc; --sec-bg: rgba(177,151,252,.12);`; const darkScope = (pre) => SCOPE.split(', ').map((s) => `${pre} ${s}`).join(', '); const CSS = ` ${SCOPE} { --bg: #f7f5f2; --panel: rgba(255,255,255,.75); --fg: #1d1b19; --soft: #5b5650; --chord: #e8590c; --chord-bg: rgba(232,89,12,.08); --line: rgba(0,0,0,.08); --sec: #6741d9; --sec-bg: rgba(103,65,217,.09); --mono: "JetBrains Mono", "Roboto Mono", ui-monospace, Menlo, Consolas, monospace; --sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; } ${darkScope('html[data-theme="dark"]')} {${DARK} } @media (prefers-color-scheme: dark) { ${darkScope('html:not([data-theme])')} {${DARK} } } /* ---------- view mode ---------- */ #${ID} { position: fixed; inset: 0; z-index: 2147483647; background: var(--bg); color: var(--fg); display: flex; flex-direction: column; font-family: var(--sans); -webkit-font-smoothing: antialiased; } #${ID} .bar { flex: 0 0 auto; display: flex; align-items: center; gap: 14px; padding: 8px 18px; background: var(--panel); backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); font-size: 13px; color: var(--soft); } #${ID} .t { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #${ID} .t b { color: var(--fg); font-weight: 700; font-size: 15px; letter-spacing: -.01em; } #${ID} .t span { margin-left: 8px; } #${ID} .hints { display: flex; gap: 12px; white-space: nowrap; } #${ID} .hints span { display: inline-flex; align-items: center; gap: 5px; } #${ID} kbd, #${ID}-banner kbd { font-family: var(--sans); font-weight: 700; font-size: 11px; color: var(--fg); padding: 2px 7px; border-radius: 5px; border: 1px solid var(--line); background: var(--bg); box-shadow: 0 1px 0 var(--line); } #${ID} .btns { display: flex; gap: 4px; } #${ID} .btns button { display: inline-flex; align-items: center; font: 600 13px var(--sans); color: var(--fg); background: transparent; cursor: pointer; border: 1px solid var(--line); border-radius: 7px; padding: 4px 10px; transition: background .15s; } #${ID} .btns button:hover { background: var(--chord-bg); } #${ID} .cols { flex: 1 1 auto; min-height: 0; overflow: hidden; padding: 14px 22px; box-sizing: border-box; column-fill: auto; column-gap: 3ch; column-rule: 1px solid var(--line); font-family: var(--mono); font-weight: 450; line-height: 1.32; } #${ID} .blk { white-space: pre-wrap; break-inside: avoid; margin: 0 0 .75em; } #${ID} .blk b { color: var(--chord); font-weight: 700; background: var(--chord-bg); border-radius: 3px; box-shadow: 0 0 0 1px var(--chord-bg); } #${ID} .sec { display: inline-block; font: 700 .72em/1.6 var(--sans); text-transform: uppercase; letter-spacing: .08em; color: var(--sec); background: var(--sec-bg); padding: 0 .6em; border-radius: 999px; } /* ---------- big hint above the song (site view) ---------- */ #${ID}-banner { display: flex; align-items: center; justify-content: center; gap: 14px; margin: 0 0 18px; padding: 16px 20px; border-radius: 14px; cursor: pointer; background: linear-gradient(135deg, #ff8a3d, #e8590c); color: #fff; font: 700 22px/1.2 var(--sans); letter-spacing: -.01em; box-shadow: 0 6px 20px rgba(232,89,12,.25); transition: transform .15s; } #${ID}-banner:hover { transform: translateY(-1px); } #${ID}-banner kbd { font-size: 22px; padding: 4px 14px; border-radius: 9px; color: #e8590c; background: #fff; border: 0; box-shadow: 0 3px 0 rgba(0,0,0,.2); } #${ID}-banner small { font-weight: 500; font-size: 14px; opacity: .85; } /* ---------- floating buttons (site view) ---------- */ #${ID}-dock { position: fixed; right: 16px; bottom: 16px; z-index: 2147483646; display: flex; gap: 8px; } #${ID}-dock button { display: inline-flex; align-items: center; font: 700 13px var(--sans); padding: 9px 14px; border-radius: 10px; background: linear-gradient(135deg, #ff8a3d, #e8590c); color: #fff; border: 0; cursor: pointer; box-shadow: 0 6px 18px rgba(232,89,12,.3); } #${ID}-dock button.theme { padding: 9px 11px; background: var(--panel); color: var(--fg); border: 1px solid var(--line); backdrop-filter: blur(8px); box-shadow: 0 6px 18px rgba(0,0,0,.15); }`; const LOCK = 'html, body { overflow: hidden !important; }'; function findPre() { return document.querySelector('#chordPage pre, main pre, article pre, pre'); } function build() { // Nicer fonts (falls back to system fonts if blocked) const fonts = document.createElement('link'); fonts.rel = 'stylesheet'; fonts.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap'; overlay = document.createElement('div'); overlay.id = ID; overlay.style.display = 'none'; overlay.innerHTML = `
${K} ${L.exit} ${KF} ${L.fullscreen} ${KT} ${L.theme} +- ${L.size}
`; overlay.prepend(fonts); cols = overlay.querySelector('.cols'); titleEl = overlay.querySelector('.t'); lockStyle = overlay.querySelector('style.lock'); overlay.querySelector('.bar').addEventListener('click', (e) => { const btn = e.target.closest('button'); const a = btn && btn.dataset.a; if (a === 'theme') toggleTheme(); if (a === 'minus') { manualScale *= 0.92; fit(); } if (a === 'plus') { manualScale *= 1.08; fit(); } if (a === 'auto') { manualScale = 1; fit(); } if (a === 'off') setEnabled(false); }); dock = document.createElement('div'); dock.id = `${ID}-dock`; dock.innerHTML = ``; toggleBtn = dock.querySelector('.open'); toggleBtn.style.display = 'none'; toggleBtn.addEventListener('click', () => setEnabled(true)); dock.querySelector('.theme').addEventListener('click', toggleTheme); themeBtns = [overlay.querySelector('[data-a="theme"]'), dock.querySelector('.theme')]; banner = document.createElement('div'); banner.id = `${ID}-banner`; banner.innerHTML = `${L.press}${K}${L.banner}`; banner.addEventListener('click', () => setEnabled(true)); } // Put our elements back if the page removed them. function ensureAttached() { if (!overlay.isConnected) { document.body.appendChild(overlay); log('overlay (re)attached'); } if (!dock.isConnected) document.body.appendChild(dock); // Banner sits right above the song; its styles come from the overlay's