diff --git a/src/plugins/better-fullscreen/index.ts b/src/plugins/better-fullscreen/index.ts new file mode 100644 index 0000000000..29b0465aa3 --- /dev/null +++ b/src/plugins/better-fullscreen/index.ts @@ -0,0 +1,400 @@ +import { createPlugin } from '@/utils'; +import style from './style.css?inline'; +import { getLyrics, type LyricResult } from './lyrics'; + +// 1. Define the Config structure +interface Config { + enabled: boolean; + perfectSync: boolean; + romanize: boolean; +} + +// 2. Define the PluginContext structure +interface PluginContext { + getConfig: () => Promise; + setConfig: (config: Config) => void; +} + +export default createPlugin({ + name: 'Better Fullscreen', + restartNeeded: true, + config: { + enabled: true, + perfectSync: false, + romanize: false, + }, + stylesheets: [style], + + menu: async ({ getConfig, setConfig }) => { + const config = await getConfig(); + return [ + { + label: 'Enable Better Fullscreen', + type: 'checkbox', + checked: config.enabled, + click: () => setConfig({ ...config, enabled: !config.enabled }), + }, + ]; + }, + + renderer: { + async start(ctx: PluginContext) { + const config = await ctx.getConfig(); + let isFullscreen = false; + let lyrics: LyricResult | null = null; + let lastSrc = ''; + let retryCount = 0; + const html = ` +
+
+
+
+
+
+
+
+
+ +
+
+ + + + + +
+
+ Perfect Sync + +
+
+ Romanize (Google) + +
+
+ +
+
+
+
+
+
+
+
Loading...
+
+
+
+ +
+
+
+
Title
+
Artist
+
+
+
+ 0:00 +
+ 0:00 +
+
+ + + +
+
+
+
+ +
+ `; + + const div = document.createElement('div'); + div.innerHTML = html; + document.body.appendChild(div); + + const ui = { + container: document.getElementById('bfs-container'), + bgLayer: document.querySelector('.bfs-bg-layer') as HTMLElement, + art: document.getElementById('bfs-art') as HTMLImageElement, + title: document.getElementById('bfs-title'), + artist: document.getElementById('bfs-artist'), + curr: document.getElementById('bfs-curr'), + dur: document.getElementById('bfs-dur'), + fill: document.getElementById('bfs-fill'), + seek: document.getElementById('bfs-seek'), + lines: document.getElementById('bfs-lines'), + scroll: document.getElementById('bfs-scroll'), + canvas: document.getElementById('bfs-canvas') as HTMLCanvasElement, + viz: document.getElementById('bfs-viz'), + playBtn: document.getElementById('bfs-play'), + prevBtn: document.getElementById('bfs-prev'), + nextBtn: document.getElementById('bfs-next'), + iconPlay: document.getElementById('bfs-icon-play'), + iconPause: document.getElementById('bfs-icon-pause'), + settingsBtn: document.getElementById('bfs-settings-btn'), + settingsModal: document.getElementById('bfs-settings-modal'), + optSync: document.getElementById('bfs-opt-sync') as HTMLInputElement, + optRoman: document.getElementById('bfs-opt-roman') as HTMLInputElement, + }; + + const updateColors = () => { + try { + const ctx = ui.canvas.getContext('2d'); + if (!ctx) return; + ctx.drawImage(ui.art, 0, 0, 50, 50); + const data = ctx.getImageData(0, 0, 50, 50).data; + const getC = (x:number, y:number) => { + const i = ((y * 50) + x) * 4; + return `rgb(${data[i]}, ${data[i+1]}, ${data[i+2]})`; + }; + document.documentElement.style.setProperty('--bfs-c1', getC(25, 25)); + document.documentElement.style.setProperty('--bfs-c2', getC(10, 10)); + document.documentElement.style.setProperty('--bfs-c3', getC(40, 40)); + document.documentElement.style.setProperty('--bfs-c4', getC(40, 10)); + document.documentElement.style.setProperty('--bfs-c5', getC(10, 40)); + } catch(e) {} + }; + + const renderLyrics = () => { + if (!ui.lines) return; + ui.lines.innerHTML = ''; + if (!lyrics) { + ui.lines.innerHTML = ` +
+ Lyrics not available + +
+ `; + document + .getElementById('bfs-force-fetch') + ?.addEventListener('click', () => { + const title = ui.title?.innerText; + const artist = ui.artist?.innerText; + const video = document.querySelector('video'); + if(title && artist && video) { + ui.lines!.innerHTML = '
Searching...
'; + performFetch(title, artist, video.duration); + } + }); + return; + } + + if (lyrics.lines) { + lyrics.lines.forEach((line) => { + const el = document.createElement('div'); + el.className = 'bfs-line'; + + const isInst = line.text.includes('...') || line.text.includes('♪') || line.text.toLowerCase().includes('instrumental'); + + if(isInst) { + el.classList.add('instrumental'); + el.innerHTML = ` +
+
+
+
+
`; + } else { + let html = `${line.text}`; + if(line.romaji) { + html += `${line.romaji}`; + } + el.innerHTML = html; + } + + el.onclick = () => { + const video = document.querySelector('video'); + if (video) video.currentTime = line.timeMs / 1000; + }; + ui.lines?.appendChild(el); + }); + } else if (lyrics.plain) { + ui.lines.innerHTML = `
${lyrics.plain}
`; + } + }; + + const syncLyrics = (time: number) => { + if (!lyrics?.lines || !ui.lines) return; + + const offset = config.perfectSync ? 0.5 : 0; + const timeMs = (time + offset) * 1000; + + let activeIdx = -1; + for (let i = 0; i < lyrics.lines.length; i++) { + if (timeMs >= lyrics.lines[i].timeMs) activeIdx = i; + else break; + } + + const domLines = ui.lines.querySelectorAll('.bfs-line'); + let isInstrumental = false; + + domLines.forEach((line: any, idx) => { + if (idx === activeIdx) { + if (!line.classList.contains('active')) { + line.classList.add('active'); + line.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + if(line.classList.contains('instrumental')) isInstrumental = true; + } else { + line.classList.remove('active'); + } + }); + + if(isInstrumental) ui.viz?.classList.add('show'); + else ui.viz?.classList.remove('show'); + }; + + const performFetch = async (title: string, artist: string, duration: number) => { + lyrics = await getLyrics(title, artist, duration, config.romanize); + renderLyrics(); + }; + + const formatTime = (s: number) => { + if (isNaN(s)) return "0:00"; + const m = Math.floor(s / 60); + const sec = Math.floor(s % 60); + return `${m}:${sec < 10 ? '0' : ''}${sec}`; + }; + + setInterval(async () => { + const video = document.querySelector('video'); + if (!video) return; + + const title = (document.querySelector('ytmusic-player-bar .title') as HTMLElement)?.innerText; + let artist = (document.querySelector('ytmusic-player-bar .byline') as HTMLElement)?.innerText; + const artSrc = (document.querySelector('.image.ytmusic-player-bar') as HTMLImageElement)?.src; + if(artist) artist = artist.split(/[•·]/)[0].trim(); + + if (ui.title && title) ui.title.innerText = title; + if (ui.artist && artist) ui.artist.innerText = artist; + + if (ui.art && artSrc) { + const highRes = artSrc.replace(/w\d+-h\d+/, 'w1200-h1200'); + if (ui.art.src !== highRes) { + ui.art.src = highRes; + ui.art.onload = updateColors; + } + } + + const currentSrc = video.src; + if (currentSrc && currentSrc !== lastSrc) { + lastSrc = currentSrc; + retryCount = 0; + if (title && artist) { + ui.lines!.innerHTML = '
Searching lyrics...
'; + performFetch(title, artist, video.duration); + } + } + + if (isFullscreen) { + ui.curr!.innerText = formatTime(video.currentTime); + ui.dur!.innerText = formatTime(video.duration); + const pct = (video.currentTime / video.duration) * 100; + ui.fill!.style.width = `${pct}%`; + syncLyrics(video.currentTime); + + if (video.paused) { + ui.iconPlay!.style.display = 'block'; + ui.iconPause!.style.display = 'none'; + } else { + ui.iconPlay!.style.display = 'none'; + ui.iconPause!.style.display = 'block'; + } + } + }, 250); + + const toggleFS = (active: boolean) => { + isFullscreen = active; + if (active) { + document.body.classList.add('bfs-active'); + document.documentElement.requestFullscreen().catch(()=>{}); + } else { + document.body.classList.remove('bfs-active'); + if (document.fullscreenElement) document.exitFullscreen().catch(()=>{}); + } + }; + + document.getElementById('bfs-close')?.addEventListener('click', () => toggleFS(false)); + window.addEventListener('keydown', e => { + if(e.key === 'F12') toggleFS(!isFullscreen); + if(e.key === 'Escape') toggleFS(false); + }); + + ui.settingsBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + ui.settingsModal?.classList.toggle('active'); + }); + ui.container?.addEventListener('click', (e) => { + if(e.target !== ui.settingsBtn && !ui.settingsModal?.contains(e.target as Node)) { + ui.settingsModal?.classList.remove('active'); + } + }); + + ui.optSync?.addEventListener('change', (e) => { + config.perfectSync = (e.target as HTMLInputElement).checked; + ctx.setConfig(config); + }); + + ui.optRoman?.addEventListener('change', async (e) => { + config.romanize = (e.target as HTMLInputElement).checked; + ctx.setConfig(config); + const title = ui.title?.innerText; + const artist = ui.artist?.innerText; + const video = document.querySelector('video'); + if(title && artist && video) { + ui.lines!.innerHTML = '
Processing...
'; + performFetch(title, artist, video.duration); + } + }); + + ui.playBtn?.addEventListener('click', () => { const v=document.querySelector('video'); if(v) v.paused?v.play():v.pause(); }); + ui.prevBtn?.addEventListener('click', () => (document.querySelector('.previous-button') as HTMLElement)?.click()); + ui.nextBtn?.addEventListener('click', () => (document.querySelector('.next-button') as HTMLElement)?.click()); + ui.seek?.addEventListener('click', (e) => { + const v = document.querySelector('video'); if(!v)return; + const rect = ui.seek!.getBoundingClientRect(); + v.currentTime = ((e.clientX - rect.left) / rect.width) * v.duration; + }); + + // --- MOVED TO ALBUM ART --- + setInterval(() => { + const artContainer = document.querySelector('#song-image'); + + if (artContainer && !document.getElementById('bfs-trigger')) { + const btn = document.createElement('div'); + btn.id = 'bfs-trigger'; + btn.title = 'Open Lyrics (Better Fullscreen)'; + btn.innerHTML = ``; + + btn.onclick = (e) => { + e.stopPropagation(); + toggleFS(true); + }; + + if(getComputedStyle(artContainer).position === 'static') { + (artContainer as HTMLElement).style.position = 'relative'; + } + + artContainer.appendChild(btn); + } + }, 1000); + } + } +}); \ No newline at end of file diff --git a/src/plugins/better-fullscreen/lyrics.ts b/src/plugins/better-fullscreen/lyrics.ts new file mode 100644 index 0000000000..c3d70ba0f8 --- /dev/null +++ b/src/plugins/better-fullscreen/lyrics.ts @@ -0,0 +1,109 @@ +export interface LyricLine { text: string; timeMs: number; romaji?: string; } +export interface LyricResult { lines?: LyricLine[]; plain?: string; synced: boolean; } + +// --- GOOGLE TRANSLATE (GTX) ROMANIZER --- +const googleRomanize = async (text: string): Promise => { + try { + if (!text || /^[\x00-\x7F]*$/.test(text)) return text; + + const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=rm&q=${encodeURIComponent(text)}`; + const res = await fetch(url); + if (!res.ok) return text; + + const data = await res.json(); + + if (data && data[0] && Array.isArray(data[0])) { + let romajiFull = ''; + + data[0].forEach((chunk: any) => { + if(Array.isArray(chunk)) { + const possibleRomaji = chunk[chunk.length - 1]; + if (typeof possibleRomaji === 'string' && possibleRomaji !== text && !possibleRomaji.includes(text)) { + romajiFull += possibleRomaji + ' '; + } + } + }); + + if (romajiFull.trim().length > 0) { + return romajiFull.trim(); + } + } + } catch (e) {} + return text; +}; + +// --- HELPERS --- +const cleanTitle = (text: string) => { + return text + .replace(/\(feat\..*?\)/i, '') + .replace(/\[feat\..*?\]/i, '') + .replace(/\(Remaster.*?\)/i, '') + .replace(/\(.*?Mix\)/i, '') + .replace(/\(.*?Version\)/i, '') + .replace(/ - .*?$/, '') + .trim(); +}; + +const parseLRC = (lrc: string): LyricLine[] => { + const lines: LyricLine[] = []; + const regex = /^\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/; + lrc.split('\n').forEach(line => { + const match = line.match(regex); + if (match) { + const min = parseInt(match[1]); + const sec = parseInt(match[2]); + const ms = parseInt(match[3].padEnd(3, '0')); + const text = match[4].trim(); + if (text) lines.push({ timeMs: (min * 60 * 1000) + (sec * 1000) + ms, text }); + } + }); + return lines; +}; + +// --- MAIN FETCHER --- +export const getLyrics = async (title: string, artist: string, duration: number, romanize: boolean = false): Promise => { + try { + const targetDur = Math.round(duration); + const q = `${cleanTitle(title)} ${artist}`; + + const searchUrl = new URL('https://lrclib.net/api/search'); + searchUrl.searchParams.append('q', q); + + const res = await fetch(searchUrl.toString()); + if(!res.ok) return null; + + const list = await res.json(); + if(!Array.isArray(list) || list.length === 0) return null; + + list.sort((a, b) => { + const aHasSync = !!a.syncedLyrics; + const bHasSync = !!b.syncedLyrics; + if(aHasSync !== bHasSync) return bHasSync ? 1 : -1; + return Math.abs(a.duration - targetDur) - Math.abs(b.duration - targetDur); + }); + + const best = list[0]; + let result: LyricResult | null = null; + + if(best.syncedLyrics) result = { lines: parseLRC(best.syncedLyrics), synced: true }; + else if(best.plainLyrics) result = { plain: best.plainLyrics, synced: false }; + + // 3. Apply Google Romanization + if (result && romanize) { + if (result.lines) { + const promises = result.lines.map(async (line) => { + const romaji = await googleRomanize(line.text); + if (romaji && romaji.toLowerCase() !== line.text.toLowerCase()) { + return { ...line, romaji }; + } + return line; + }); + result.lines = await Promise.all(promises); + } + } + + return result; + + } catch (e) { console.warn('Lyrics Error:', e); } + return null; +}; \ No newline at end of file diff --git a/src/plugins/better-fullscreen/style.css b/src/plugins/better-fullscreen/style.css new file mode 100644 index 0000000000..9797148fb2 --- /dev/null +++ b/src/plugins/better-fullscreen/style.css @@ -0,0 +1,295 @@ +:root { + /* Default colors */ + --bfs-c1: #444; + --bfs-c2: #555; + --bfs-c3: #666; + --bfs-c4: #333; + --bfs-c5: #222; + --bfs-font: "YouTube Sans", Roboto, "Segoe UI", Helvetica, Arial, sans-serif; +} + +#bfs-container { + position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; + z-index: 2147483647; background: #000; + color: #fff; opacity: 0; pointer-events: none; visibility: hidden; + font-family: var(--bfs-font); + transition: opacity 0.4s ease; + overflow: hidden; +} + +body.bfs-active #bfs-container { + opacity: 1; pointer-events: all; visibility: visible; +} + +/* --- Multi-Color Animated Background --- */ +.bfs-bg-layer { + position: absolute; inset: 0; z-index: 0; + background: #050505; overflow: hidden; +} + +.bfs-blob { + position: absolute; border-radius: 50%; + filter: blur(80px); opacity: 0.6; + animation: bfs-float 20s infinite alternate ease-in-out; + transform-origin: center center; +} + +/* 5 Blobs for richer color */ +.bfs-blob-1 { width: 70vw; height: 70vw; background: var(--bfs-c1); top: -20%; left: -10%; animation-delay: 0s; } +.bfs-blob-2 { width: 60vw; height: 60vw; background: var(--bfs-c2); top: -10%; right: -20%; animation-delay: -5s; } +.bfs-blob-3 { width: 60vw; height: 60vw; background: var(--bfs-c3); bottom: -20%; left: -10%; animation-delay: -10s; } +.bfs-blob-4 { width: 50vw; height: 50vw; background: var(--bfs-c4); bottom: -10%; right: -10%; animation-delay: -15s; } +.bfs-blob-5 { width: 40vw; height: 40vw; background: var(--bfs-c5); top: 30%; left: 30%; animation-delay: -7s; opacity: 0.4; } + +@keyframes bfs-float { + 0% { transform: translate(0, 0) scale(1) rotate(0deg); } + 50% { transform: translate(30px, -30px) scale(1.1) rotate(5deg); } + 100% { transform: translate(-20px, 20px) scale(0.95) rotate(-5deg); } +} + +.bfs-overlay { + position: absolute; inset: 0; z-index: 1; + background: rgba(0,0,0,0.3); + backdrop-filter: blur(50px); +} + +/* --- Layout --- */ +.bfs-content { + position: relative; z-index: 10; width: 100%; height: 100%; + display: grid; grid-template-columns: 55% 45%; + align-items: center; +} + +/* --- Left Side: Lyrics --- */ +.bfs-lyrics-section { + height: 100vh; position: relative; + display: flex; flex-direction: column; + padding-left: 8vw; + mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%); +} + +.bfs-visualizer-icon { + position: absolute; top: 50%; left: 8vw; + transform: translateY(-220px); + display: flex; gap: 4px; align-items: flex-end; + height: 24px; width: 30px; + opacity: 0; transition: opacity 0.5s ease; + z-index: 20; pointer-events: none; +} +.bfs-visualizer-icon.show { opacity: 1; } + +.bfs-viz-bar { + width: 4px; background: #fff; border-radius: 2px; + animation: bfs-bounce 1s infinite ease-in-out; +} +.bfs-viz-bar:nth-child(1) { height: 12px; animation-delay: 0s; } +.bfs-viz-bar:nth-child(2) { height: 24px; animation-delay: 0.2s; } +.bfs-viz-bar:nth-child(3) { height: 16px; animation-delay: 0.4s; } +@keyframes bfs-bounce { 0%, 100% { transform: scaleY(0.5); } 50% { transform: scaleY(1); } } + +.bfs-lyrics-scroll { + height: 100%; overflow-y: auto; scrollbar-width: none; + scroll-behavior: smooth; +} +.bfs-lyrics-scroll::-webkit-scrollbar { display: none; } + +.bfs-lyrics-wrapper { + padding: 50vh 0; width: 100%; max-width: 900px; +} + +/* Lyric Lines */ +.bfs-line { + font-size: 3.4rem; font-weight: 700; line-height: 1.3; + margin-bottom: 35px; color: rgba(255,255,255,0.5); + cursor: pointer; transform-origin: left center; + transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); + filter: blur(0.5px); transform: scale(0.98); + display: flex; flex-direction: column; align-items: flex-start; +} + +.bfs-line.active { + color: #fff; opacity: 1; filter: blur(0); transform: scale(1); + text-shadow: 0 0 40px rgba(0,0,0,0.3); +} + +.bfs-line:hover:not(.active) { color: rgba(255,255,255,0.8); filter: blur(0); } + +/* Romaji Styling */ +.bfs-romaji { + display: block; + font-size: 0.55em; + font-weight: normal; + color: rgba(255, 255, 255, 0.75); + margin-top: 8px; + line-height: 1.4; + letter-spacing: 0.02em; +} + +/* Instrumental Icon */ +.bfs-line.instrumental { + height: 60px; display: flex; flex-direction: row; align-items: center; margin-bottom: 40px; +} +.bfs-viz-icon { + display: flex; gap: 6px; align-items: flex-end; + height: 30px; width: 40px; opacity: 0.5; transition: opacity 0.3s; +} +.bfs-line.active .bfs-viz-icon { opacity: 1; } + +.bfs-empty { font-size: 1.8rem; opacity: 0.5; font-weight: 500; display: flex; flex-direction: column; gap: 15px; align-items: flex-start; } +.bfs-refresh-btn { + font-size: 1rem; padding: 10px 20px; border-radius: 20px; + border: 1px solid rgba(255,255,255,0.3); background: rgba(255,255,255,0.1); + color: #fff; cursor: pointer; transition: all 0.2s; + display: flex; align-items: center; gap: 8px; +} +.bfs-refresh-btn:hover { background: rgba(255,255,255,0.25); border-color: #fff; } + +/* --- Meta & Controls --- */ +.bfs-meta-section { + padding: 4vw; display: flex; flex-direction: column; + justify-content: center; align-items: center; + height: 100vh; box-sizing: border-box; +} + +.bfs-art { + width: 100%; max-width: 380px; aspect-ratio: 1/1; + border-radius: 8px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + margin-bottom: 40px; background: #222; overflow: hidden; + transition: transform 0.5s ease; + flex-shrink: 1; max-height: 40vh; +} +.bfs-art img { width: 100%; height: 100%; object-fit: cover; } + +.bfs-info { margin-bottom: 40px; text-align: left; width: 100%; max-width: 380px; flex-shrink: 0; } +.bfs-title { + font-size: 2rem; font-weight: 700; margin-bottom: 8px; color: #fff; + line-height: 1.2; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; +} +/* ARTIST FONT BIGGER */ +.bfs-artist { + font-size: 1.4rem; + font-weight: normal; + color: rgba(255,255,255,0.7); +} + +.bfs-controls-container { width: 100%; max-width: 380px; flex-shrink: 0; } + +.bfs-progress-row { + display: flex; align-items: center; gap: 12px; margin-bottom: 25px; + font-size: 0.8rem; font-weight: 500; color: rgba(255,255,255,0.5); font-variant-numeric: tabular-nums; +} +.bfs-bar-bg { + flex-grow: 1; height: 4px; background: rgba(255,255,255,0.2); border-radius: 4px; cursor: pointer; +} +.bfs-bar-fill { height: 100%; background: #fff; width: 0%; border-radius: 4px; } + +.bfs-buttons { + display: flex; align-items: center; justify-content: center; gap: 40px; width: 100%; +} +.bfs-btn { + background: none; border: none; color: #fff; cursor: pointer; + display: flex; align-items: center; justify-content: center; transition: transform 0.2s; +} +.bfs-btn:hover { transform: scale(1.1); } +.bfs-play-btn svg { width: 56px; height: 56px; fill: currentColor; } +.bfs-skip-btn svg { width: 28px; height: 28px; fill: currentColor; opacity: 0.7; } + +/* Top Buttons */ +#bfs-close { + position: absolute; top: 30px; right: 30px; + width: 44px; height: 44px; border-radius: 50%; + background: rgba(255,255,255,0.1); border: none; color: #fff; + cursor: pointer; z-index: 100; display: flex; align-items: center; justify-content: center; + backdrop-filter: blur(10px); opacity: 0; transition: opacity 0.3s ease, background 0.2s; +} +.bfs-corner-zone { + position: absolute; top: 0; width: 150px; height: 150px; z-index: 99; +} +.bfs-zone-left { left: 0; } +.bfs-zone-right { right: 0; } + +.bfs-zone-right:hover ~ #bfs-close, #bfs-close:hover { opacity: 1; } +.bfs-zone-left:hover ~ #bfs-settings-btn, #bfs-settings-btn:hover { opacity: 1; } + +#bfs-close:hover { background: rgba(255,255,255,0.25); } + +#bfs-settings-btn { + position: absolute; top: 30px; left: 30px; + width: 44px; height: 44px; border-radius: 50%; + background: rgba(255,255,255,0.1); border: none; color: #fff; + cursor: pointer; z-index: 100; display: flex; align-items: center; justify-content: center; + backdrop-filter: blur(10px); opacity: 0; transition: opacity 0.3s ease, background 0.2s; +} +#bfs-settings-btn:hover { background: rgba(255,255,255,0.25); } + +/* Settings Modal */ +#bfs-settings-modal { + position: absolute; top: 85px; left: 30px; + width: 280px; background: rgba(30, 30, 30, 0.95); + backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.1); + border-radius: 12px; padding: 15px; z-index: 200; + transform-origin: top left; transform: scale(0.95); + opacity: 0; pointer-events: none; transition: all 0.2s; + box-shadow: 0 10px 40px rgba(0,0,0,0.5); +} +#bfs-settings-modal.active { opacity: 1; pointer-events: all; transform: scale(1); } + +.bfs-setting-item { + display: flex; justify-content: space-between; align-items: center; + padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.1); + color: rgba(255,255,255,0.9); font-size: 0.9rem; font-weight: 500; +} +.bfs-setting-item:last-child { border-bottom: none; } + +.bfs-toggle { position: relative; width: 40px; height: 22px; cursor: pointer; } +.bfs-toggle input { opacity: 0; width: 0; height: 0; } +.bfs-slider { + position: absolute; inset: 0; background-color: rgba(255,255,255,0.2); + border-radius: 22px; transition: .3s; +} +.bfs-slider:before { + position: absolute; content: ""; height: 16px; width: 16px; + left: 3px; bottom: 3px; background-color: white; + border-radius: 50%; transition: .3s; +} +.bfs-toggle input:checked + .bfs-slider { background-color: #3b82f6; } +.bfs-toggle input:checked + .bfs-slider:before { transform: translateX(18px); } + +/* --- TRIGGER BUTTON ON ALBUM ART --- */ +#bfs-trigger { + position: absolute; + bottom: 20px; + left: 20px; + z-index: 9999 !important; + width: 48px; height: 48px; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + cursor: pointer; + color: #fff; + border: 1px solid rgba(255,255,255,0.15); + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + + opacity: 0; + pointer-events: none; + transform: scale(0.9); + transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +#song-image:hover #bfs-trigger, +ytmusic-player:hover #bfs-trigger, +#bfs-trigger:hover { + opacity: 1; + pointer-events: auto; + transform: scale(1); +} + +#bfs-trigger:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +#bfs-canvas { display: none; } \ No newline at end of file