// ==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 };
})();