您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Sleek SoundCloud dashboard: auto client_id, search, favorites, history, and thumbnail fixes. Opens tracks in new tabs
// ==UserScript== // @name Volumer: SoundCloud Dashboard // @namespace https://twisk.fun/ // @version 1.0 // @description Sleek SoundCloud dashboard: auto client_id, search, favorites, history, and thumbnail fixes. Opens tracks in new tabs // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect soundcloud.com // @connect www.soundcloud.com // @connect api-v2.soundcloud.com // @connect raw.githubusercontent.com // @connect a-v2.sndcdn.com // @connect i1.sndcdn.com // @connect i2.sndcdn.com // @connect cdn.sndcdn.com // @connect media.soundcloud.com // @icon https://github.com/pillowslua/crackduo/blob/main/Untitled%20design.png?raw=true // @license MIT // @author Airplane Modz // ==/UserScript== (function () { 'use strict'; /* ============================ Helper wrappers for GM_xmlhttpRequest - gmRequest: text / json - gmRequestBinary: returns Blob ============================ */ function gmRequest(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url, headers: options.headers || {}, data: options.body || null, responseType: options.responseType || 'text', onload(res) { if (res.status >= 200 && res.status < 400) resolve(res); else reject(new Error('HTTP ' + res.status + ' ' + url)); }, onerror(err) { reject(err); }, ontimeout() { reject(new Error('Timeout ' + url)); } }); }); } async function gmFetchJson(url, options = {}) { const res = await gmRequest(url, options); try { return JSON.parse(res.responseText); } catch (e) { throw new Error('Invalid JSON from ' + url); } } async function gmRequestBinary(url) { // returns Blob const res = await gmRequest(url, { responseType: 'arraybuffer' }); const buffer = res.response; // Build blob. Try to get content-type header if available let contentType = 'image/jpeg'; if (res.responseHeaders) { const m = /content-type:\s*([^\r\n;]+)/i.exec(res.responseHeaders); if (m) contentType = m[1].trim(); } try { const blob = new Blob([buffer], { type: contentType }); return blob; } catch (e) { // fallback: create blob without type return new Blob([buffer]); } } /* ============================ Storage keys + defaults ============================ */ const KEYS = { CLIENT_ID: 'sc_client_id_v1', THEME: 'sc_theme_v1', SONGS: 'sc_songs_v1', USERS: 'sc_users_v1', SESSION: 'sc_session_v1' }; const DEFAULT_RAW = [ "https://soundcloud.com/forss/flickermood", "https://soundcloud.com/odesza/a-moment-apart", "https://soundcloud.com/madeon/imperium" ]; /* ============================ Auto fetch client_id (persist) ============================ */ async function autoFetchClientId(force = false) { const cached = localStorage.getItem(KEYS.CLIENT_ID); if (cached && !force) return cached; showStatus('Fetching SoundCloud client_id...'); try { const homepage = (await gmRequest('https://soundcloud.com')).responseText; const jsMatches = Array.from(homepage.matchAll(/<script[^>]+src="([^"]+\.js)"/g)).map(m => m[1]); const uniq = [...new Set(jsMatches)].map(s => s.startsWith('http') ? s : ('https://soundcloud.com' + s)); for (const url of uniq) { try { const res = await gmRequest(url); const code = res.responseText; let m = code.match(/client_id["']?\s*[:=]\s*["']([a-zA-Z0-9-_]{16,})["']/) || code.match(/client_id\s*:\s*["']([a-zA-Z0-9-_]{16,})["']/) || code.match(/client_id=([a-zA-Z0-9-_]{16,})/); if (m && m[1]) { localStorage.setItem(KEYS.CLIENT_ID, m[1]); showStatus('client_id obtained'); setTimeout(hideStatus, 600); return m[1]; } } catch (e) { // ignore script fetch errors } } throw new Error('client_id not found'); } catch (e) { hideStatus(); throw e; } } async function ensureClientId() { let cid = localStorage.getItem(KEYS.CLIENT_ID); if (cid) return cid; try { cid = await autoFetchClientId(false); return cid; } catch (e) { // ask manual fallback const manual = prompt('Auto-fetch client_id failed. Paste SoundCloud client_id (from DevTools):'); if (manual && manual.trim().length >= 8) { localStorage.setItem(KEYS.CLIENT_ID, manual.trim()); return manual.trim(); } throw new Error('No client_id available'); } } /* ============================ Helpers UI: status spinner ============================ */ let statusEl = null; function showStatus(text) { if (!statusEl) { statusEl = document.createElement('div'); statusEl.style = 'position:fixed;right:18px;top:18px;padding:8px 12px;background:#111;color:#fff;border-radius:8px;z-index:9999999;font-family:sans-serif;box-shadow:0 6px 20px rgba(0,0,0,0.3)'; document.body.appendChild(statusEl); if (!document.getElementById('scdash-spin-style')) { const s = document.createElement('style'); s.id = 'scdash-spin-style'; s.textContent = '@keyframes scdashspin{to{transform:rotate(360deg);}} .scdash-spinner{display:inline-block;width:14px;height:14px;border-radius:50%;border:2px solid rgba(255,255,255,.18);border-top-color:#fff;animation:scdashspin 1s linear infinite;}'; document.head.appendChild(s); } } statusEl.innerHTML = `<span style="margin-right:8px">${text}</span><span class="scdash-spinner"></span>`; } function hideStatus() { if (statusEl) { statusEl.remove(); statusEl = null; } } /* ============================ UI skeleton + styles ============================ */ GM_addStyle(` .scdash-openbtn{position:fixed;left:18px;bottom:18px;z-index:2147483646;background:#ff5500;color:#fff;border:none;padding:10px 14px;border-radius:24px;box-shadow:0 6px 18px rgba(0,0,0,.25);cursor:pointer;font-weight:700;font-family:Inter,system-ui,Arial;} .scdash-modal{position:fixed;top:60px;left:60px;width:820px;height:640px;background:var(--bg);color:var(--fg);z-index:2147483646;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.4);display:flex;flex-direction:column;resize:both;overflow:hidden;font-family:Inter,system-ui,Arial} .scdash-header{display:flex;align-items:center;gap:12px;padding:12px 16px;background:linear-gradient(90deg,#ff7a2f,#ff4f00);color:#fff} .scdash-title{font-size:18px;font-weight:800} .scdash-body{display:flex;flex:1;overflow:hidden} .scdash-left{width:62%;padding:12px;overflow:auto;background:rgba(255,255,255,0.03)} .scdash-right{width:38%;padding:12px;overflow:auto;background:#fbfbfc;border-left:1px solid rgba(0,0,0,0.04)} .scdash-controls{margin-left:auto;display:flex;gap:8px} .scdash-input{width:100%;padding:8px;border-radius:8px;border:1px solid #ddd;margin-bottom:8px} .scdash-btn{background:#3182ce;color:#fff;border:none;padding:8px 10px;border-radius:8px;cursor:pointer} .sc-track{display:flex;gap:12px;padding:10px;border-radius:8px;margin-bottom:10px;background:#fff;align-items:center;box-shadow:0 2px 6px rgba(0,0,0,.03)} .sc-art{width:72px;height:72px;border-radius:8px;background:#eee;overflow:hidden;flex:none;display:flex;align-items:center;justify-content:center} .sc-meta{flex:1;min-width:0} .sc-title{font-weight:700;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .sc-author{color:#666;font-size:13px;margin-top:4px} .sc-actions{display:flex;gap:8px;align-items:center} .sc-playbtn{background:#ff5500;color:#fff;border:none;padding:6px 10px;border-radius:8px;cursor:pointer} .sc-small{font-size:12px;color:#666} .sc-player-area{margin-bottom:12px;background:#fff;border-radius:8px;padding:8px;box-shadow:0 2px 8px rgba(0,0,0,.04)} .sc-empty{color:#888;padding:10px;text-align:center} .sc-history-item{padding:8px;border-radius:8px;margin-bottom:8px;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.03)} .sc-tabbtn{padding:6px 8px;border-radius:8px;border:1px solid #ddd;cursor:pointer;background:#fff} .sc-tabbtn.active{background:#3182ce;color:#fff} .sc-search-row{display:flex;gap:8px;margin-bottom:8px} :root.sc-dark{--bg:#131313;--fg:#eee} :root{--bg:#fff;--fg:#111} .scdash-modal{background:var(--bg);color:var(--fg);} @media (max-width:900px){ .scdash-modal{width:92%;height:80%} } `); /* Open button */ const openBtn = document.createElement('button'); openBtn.className = 'scdash-openbtn'; openBtn.textContent = 'SoundCloud Dashboard'; document.body.appendChild(openBtn); /* Save/Restore panel pos/size */ function savePanelState(panel) { try { localStorage.setItem('sc_panel_state', JSON.stringify({ left: panel.style.left, top: panel.style.top, width: panel.style.width, height: panel.style.height })); } catch (e) {} } function restorePanelState(panel) { try { const st = JSON.parse(localStorage.getItem('sc_panel_state') || 'null'); if (!st) return; if (st.left) panel.style.left = st.left; if (st.top) panel.style.top = st.top; if (st.width) panel.style.width = st.width; if (st.height) panel.style.height = st.height; } catch (e) {} } /* ============================ Business state (users, songs, favs, history) ============================ */ function getUsers() { return JSON.parse(localStorage.getItem(KEYS.USERS) || '{}'); } function saveUsers(u) { localStorage.setItem(KEYS.USERS, JSON.stringify(u)); } function getSessionUser() { return sessionStorage.getItem(KEYS.SESSION) || null; } function setSessionUser(u) { if (u) sessionStorage.setItem(KEYS.SESSION, u); else sessionStorage.removeItem(KEYS.SESSION); } function favKey(user) { return 'sc_favs_' + (user || 'public'); } function histKey(user) { return 'sc_hist_' + (user || 'public'); } function getFavs(user) { return JSON.parse(localStorage.getItem(favKey(user)) || '[]'); } function saveFavs(user, arr) { localStorage.setItem(favKey(user), JSON.stringify(arr)); } function getHist(user) { return JSON.parse(localStorage.getItem(histKey(user)) || '[]'); } function saveHist(user, arr) { localStorage.setItem(histKey(user), JSON.stringify(arr)); } function getSongsRaw() { try { return JSON.parse(localStorage.getItem(KEYS.SONGS)) || DEFAULT_RAW.slice(); } catch(e) { return DEFAULT_RAW.slice(); } } function saveSongsRaw(arr) { localStorage.setItem(KEYS.SONGS, JSON.stringify(arr)); } /* ============================ Modal creation + behavior ============================ */ let modal = null; async function openModal() { if (modal) return; // ensure client id is fetched before open (as requested) try { showStatus('Preparing SoundCloud (fetch client_id)...'); await ensureClientId(); showStatus('Ready'); setTimeout(hideStatus, 500); } catch (e) { hideStatus(); if (!confirm('Auto-fetch client_id failed: '+e.message+'\nContinue without client_id (Search disabled)?')) return; } modal = document.createElement('div'); modal.className = 'scdash-modal'; modal.style.left = '60px'; modal.style.top = '60px'; modal.innerHTML = ` <div class="scdash-header"> <div class="scdash-title">Recommend Songs</div> <div class="scdash-controls"> <button id="sc-refresh-client" class="scdash-btn" title="Refetch client_id">Refresh client_id</button> <button id="sc-theme-toggle" class="scdash-btn">Theme</button> <button id="sc-join-discord" class="scdash-btn">Join Discord Server</button> <button id="sc-close" class="scdash-btn">Close</button> </div> </div> <div class="scdash-body"> <div class="scdash-left"> <div style="display:flex;gap:8px;margin-bottom:8px;align-items:center"> <input id="sc-remote-url" class="scdash-input" placeholder="Remote raw file URL (raw.githubusercontent.com or public raw)"> <button id="sc-load-remote" class="scdash-btn">Load</button> </div> <div class="sc-search-row"> <input id="sc-search-input" class="scdash-input" placeholder="Search SoundCloud (press Enter)" /> <button id="sc-search-btn" class="scdash-btn">Search</button> </div> <div id="sc-player-area" class="sc-player-area"> <div id="sc-embed-holder" class="sc-empty">No track selected</div> </div> <div style="display:flex;align-items:center;justify-content:space-between"> <div style="font-weight:700">Recommend Songs</div> <div><span id="sc-total">0</span> items</div> </div> <div id="sc-list" style="margin-top:8px;overflow:auto;max-height:330px"></div> </div> <div class="scdash-right"> <div style="margin-bottom:8px"> <div style="font-weight:800;margin-bottom:6px">Account</div> <div id="sc-account-area"></div> </div> <div style="margin-top:8px"> <div style="font-weight:800;margin-bottom:6px">Favorites</div> <div id="sc-fav-list" style="max-height:220px;overflow:auto"></div> <hr style="margin:12px 0"> <div style="font-weight:800;margin-bottom:6px">History</div> <div id="sc-history-list" style="max-height:220px;overflow:auto"></div> </div> </div> </div> `; document.body.appendChild(modal); restorePanelState(modal); // draggable header const header = modal.querySelector('.scdash-header'); let dragging=false, ox=0, oy=0; header.addEventListener('mousedown', e=>{ dragging=true; ox=e.clientX-modal.offsetLeft; oy=e.clientY-modal.offsetTop; }); document.addEventListener('mousemove', e=>{ if (!dragging) return; modal.style.left=(e.clientX-ox)+'px'; modal.style.top=(e.clientY-oy)+'px'; }); document.addEventListener('mouseup', ()=>{ if (dragging) { dragging=false; savePanelState(modal); } }); // close modal.querySelector('#sc-close').addEventListener('click', ()=>{ modal.remove(); modal=null; }); // theme toggle const curTheme = localStorage.getItem(KEYS.THEME) || 'light'; applyTheme(curTheme); modal.querySelector('#sc-theme-toggle').addEventListener('click', ()=> { const t = (localStorage.getItem(KEYS.THEME) || 'light') === 'dark' ? 'light' : 'dark'; applyTheme(t); }); // join discord modal.querySelector('#sc-join-discord').addEventListener('click', () => { window.open('https://discord.gg/m3EV55SpYw', '_blank'); }); // refresh client id modal.querySelector('#sc-refresh-client').addEventListener('click', async ()=> { try { showStatus('Refetching client_id...'); await autoFetchClientId(true); alert('client_id refreshed'); } catch(e) { alert('Refetch failed: '+e.message); } finally { hideStatus(); } }); // account area renderAccountArea(); // load remote file modal.querySelector('#sc-load-remote').addEventListener('click', async ()=>{ const url = modal.querySelector('#sc-remote-url').value.trim(); if (!url) return alert('Paste remote raw file URL first.'); try { showStatus('Loading remote file...'); const res = await gmRequest(url); const lines = res.responseText.split(/\r?\n/).map(s => s.trim()).filter(Boolean); saveSongsRaw(lines); renderSongsList(); hideStatus(); alert('Loaded ' + lines.length + ' links.'); } catch (e) { hideStatus(); alert('Failed to load: ' + e.message); } }); // search handlers modal.querySelector('#sc-search-input').addEventListener('keydown', async (e)=>{ if (e.key === 'Enter') { const q = e.target.value.trim(); if (!q) return; try { showStatus('Searching...'); const results = await scSearch(q); renderSearchResults(results); } catch (err) { alert('Search error: '+err.message); } finally { hideStatus(); } } }); modal.querySelector('#sc-search-btn').addEventListener('click', async ()=>{ const q = modal.querySelector('#sc-search-input').value.trim(); if (!q) return; try { showStatus('Searching...'); const results = await scSearch(q); renderSearchResults(results); } catch (err) { alert('Search error: '+err.message); } finally { hideStatus(); } }); // initial content renderSongsList(); renderFavHistory(); } function applyTheme(t) { localStorage.setItem(KEYS.THEME, t); if (t === 'dark') document.documentElement.classList.add('sc-dark'); else document.documentElement.classList.remove('sc-dark'); } /* ============================ Render / actions for songs list ============================ */ async function renderSongsList() { if (!modal) return; const list = getSongsRaw(); const container = modal.querySelector('#sc-list'); container.innerHTML = ''; modal.querySelector('#sc-total').textContent = list.length; for (const url of list) { const row = await createTrackRow(url); container.appendChild(row); } } async function createTrackRow(url) { const div = document.createElement('div'); div.className = 'sc-track'; div.innerHTML = ` <div class="sc-art"><img src="" style="width:100%;height:100%;object-fit:cover"></div> <div class="sc-meta"><div class="sc-title">${url}</div><div class="sc-author">${url}</div></div> <div class="sc-actions"><button class="sc-playbtn">Open</button><button class="scdash-btn sc-fav-btn">♡</button></div> `; const img = div.querySelector('.sc-art img'); const titleEl = div.querySelector('.sc-title'); const authorEl = div.querySelector('.sc-author'); // fetch oEmbed metadata via GM (avoids CORS) try { const oe = await gmFetchJson('https://soundcloud.com/oembed?format=json&url=' + encodeURIComponent(url)); if (oe.title) titleEl.textContent = oe.title; if (oe.author_name) authorEl.textContent = oe.author_name; if (oe.thumbnail_url) { // fetch image binary and set object URL try { const blob = await gmRequestBinary(oe.thumbnail_url); const obj = URL.createObjectURL(blob); // revoke previous if any later - store on dataset const prev = img.dataset._objurl; if (prev) URL.revokeObjectURL(prev); img.dataset._objurl = obj; img.src = obj; } catch (imgErr) { // fallback to direct url (may be blocked), set alt blank img.src = oe.thumbnail_url; } } } catch (e) { // quietly ignore metadata fetch failing } // open button div.querySelector('.sc-playbtn').addEventListener('click', ()=> { window.open(url, '_blank'); const user = getSessionUser(); const hist = getHist(user); hist.unshift(url); saveHist(user, Array.from(new Set(hist)).slice(0,200)); renderFavHistory(); }); // fav button div.querySelector('.sc-fav-btn').addEventListener('click', ()=> { const user = getSessionUser(); const favs = getFavs(user); if (!favs.includes(url)) favs.unshift(url); saveFavs(user, favs.slice(0,200)); renderFavHistory(); }); return div; } /* ============================ Search (uses scSearch wrapper) ============================ */ async function scSearch(q) { const cid = await ensureClientId(); const url = `https://api-v2.soundcloud.com/search/tracks?q=${encodeURIComponent(q)}&client_id=${encodeURIComponent(cid)}&limit=30`; const res = await gmRequest(url); try { const json = JSON.parse(res.responseText); return json.collection || []; } catch (e) { return []; } } function renderSearchResults(results) { if (!modal) return; const container = modal.querySelector('#sc-list'); container.innerHTML = ''; if (!results || results.length === 0) { container.innerHTML = '<div class="sc-empty">No results</div>'; return; } for (const t of results) { const url = t.permalink_url || (t.user && t.user.permalink ? `https://soundcloud.com/${t.user.permalink}/${t.permalink}` : ''); const row = document.createElement('div'); row.className = 'sc-track'; row.innerHTML = ` <div class="sc-art"><img src="" style="width:100%;height:100%;object-fit:cover"></div> <div class="sc-meta"><div class="sc-title">${escapeHtml(t.title)}</div><div class="sc-author">${escapeHtml(t.user && t.user.username ? t.user.username : '')}</div></div> <div class="sc-actions"><button class="sc-playbtn">Open</button><button class="scdash-btn sc-fav-btn">♡</button></div> `; const img = row.querySelector('.sc-art img'); // artwork_url may contain {size}, remove - use original const artwork = t.artwork_url || (t.user && t.user.avatar_url) || ''; if (artwork) { // try to fetch binary and set obj url gmRequestBinary(artwork).then(blob => { const obj = URL.createObjectURL(blob); const prev = img.dataset._objurl; if (prev) URL.revokeObjectURL(prev); img.dataset._objurl = obj; img.src = obj; }).catch(_ => { img.src = artwork; }); } row.querySelector('.sc-playbtn').addEventListener('click', ()=> { window.open(url, '_blank'); const user = getSessionUser(); const h = getHist(user); h.unshift(url); saveHist(user, Array.from(new Set(h)).slice(0,200)); renderFavHistory(); }); row.querySelector('.sc-fav-btn').addEventListener('click', ()=> { const user = getSessionUser(); const f = getFavs(user); if (!f.includes(url)) f.unshift(url); saveFavs(user, f.slice(0,200)); renderFavHistory(); }); container.appendChild(row); } } /* ============================ Play embed (removed, replaced with open in new tab) ============================ */ function playUrl(url) { if (!modal) return; const holder = modal.querySelector('#sc-embed-holder'); holder.innerHTML = `<div class="sc-empty">Track opened in new tab</div>`; window.open(url, '_blank'); } /* ============================ Account area, favorites & history UI ============================ */ function renderAccountArea() { if (!modal) return; const area = modal.querySelector('#sc-account-area'); const sessionUser = getSessionUser(); area.innerHTML = ''; if (!sessionUser) { area.innerHTML = ` <input id="sc-username" class="scdash-input" placeholder="Username"> <input id="sc-password" class="scdash-input" placeholder="Password" type="password"> <div style="display:flex;gap:8px"><button id="sc-signup" class="scdash-btn">Sign Up</button><button id="sc-login" class="scdash-btn">Login</button></div> `; modal.querySelector('#sc-signup').addEventListener('click', ()=>{ const u = modal.querySelector('#sc-username').value.trim(); const p = modal.querySelector('#sc-password').value.trim(); if (!u || !p) return alert('Enter username & password'); const users = getUsers(); if (users[u]) return alert('User exists'); users[u] = p; saveUsers(users); alert('Created - login now'); }); modal.querySelector('#sc-login').addEventListener('click', ()=>{ const u = modal.querySelector('#sc-username').value.trim(); const p = modal.querySelector('#sc-password').value.trim(); const users = getUsers(); if (users[u] && users[u] === p) { setSessionUser(u); renderAccountArea(); renderFavHistory(); alert('Logged in as ' + u) } else alert('Invalid'); }); } else { area.innerHTML = `<div style="font-weight:700">${sessionUser}</div><div style="margin-top:8px"><button id="sc-logout" class="scdash-btn">Logout</button></div>`; modal.querySelector('#sc-logout').addEventListener('click', ()=>{ setSessionUser(null); renderAccountArea(); renderFavHistory(); }); } } function renderFavHistory() { if (!modal) return; const user = getSessionUser(); const favContainer = modal.querySelector('#sc-fav-list'); const histContainer = modal.querySelector('#sc-history-list'); const favs = getFavs(user), hist = getHist(user); favContainer.innerHTML = ''; histContainer.innerHTML = ''; if (!favs || favs.length === 0) favContainer.innerHTML = '<div class="sc-empty">No favorites</div>'; else favs.forEach(f => { const item = document.createElement('div'); item.className = 'sc-history-item'; item.innerHTML = `<div style="font-weight:700">${f}</div><div style="margin-top:6px"><button class="sc-playbtn">Open</button> <button class="scdash-btn sc-remove">Remove</button></div>`; item.querySelector('.sc-playbtn').addEventListener('click', ()=> { playUrl(f); const h = getHist(user); h.unshift(f); saveHist(user, Array.from(new Set(h)).slice(0,200)); renderFavHistory(); }); item.querySelector('.sc-remove').addEventListener('click', ()=> { const arr = getFavs(user).filter(x=>x!==f); saveFavs(user, arr); renderFavHistory(); }); favContainer.appendChild(item); }); if (!hist || hist.length === 0) histContainer.innerHTML = '<div class="sc-empty">No history</div>'; else hist.forEach(h => { const item = document.createElement('div'); item.className = 'sc-history-item'; item.innerHTML = `<div>${h}</div><div style="margin-top:6px"><button class="sc-playbtn">Open</button></div>`; item.querySelector('.sc-playbtn').addEventListener('click', ()=> playUrl(h)); histContainer.appendChild(item); }); } /* ============================ Utilities ============================ */ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } /* ============================ Init / Bind open button ============================ */ openBtn.addEventListener('click', async ()=>{ try { showStatus('Preparing client_id...'); await ensureClientId(); } catch(e) { /* allow continue with prompt later */ } finally { hideStatus(); } openModal(); }); // keyboard toggle Ctrl+Shift+S document.addEventListener('keydown', (e)=>{ if (e.ctrlKey && e.shiftKey && e.key.toLowerCase()==='s') { if (modal) { modal.remove(); modal=null; } else openBtn.click(); } }); // initial apply theme if ((localStorage.getItem(KEYS.THEME) || 'light') === 'dark') document.documentElement.classList.add('sc-dark'); // small exposure for debugging window.SCDASH = { autoFetchClientId, ensureClientId, scSearch: scSearch, gmRequest, gmRequestBinary }; })();