// @ts-nocheck // NAME: Full Screen Mode // AUTHOR: daksh2k // DESCRIPTION: Fancy artwork and track status display. /// const translations = { "en-US": { langName: "English", context: { queue: "Playing from queue", track: "Playing track", album: "Playing from album", artist: "Playing from artist", playlist: "Playing from playlist", playlistFolder: "Playing from playlist folder", search: "Playing from search", searchDest: "Songs", collection: "Playing from collection", likedSongs: "Liked Songs", trackRadio: "Playing from track radio", artistRadio: "Playing from artist radio", albumRadio: "Playing from album radio", playlistRadio: "Playing from playlist radio", }, upnext: "UP NEXT", unknownArtist: "Artist (Unavailable)", settings: { switchToTV: "Switch to TV Mode", switchToFullscreen: "Switch to Default Mode", tvModeConfig: "TV Mode Configuration", fullscreenConfig: "Full Screen Configuration", exit: "Exit", pluginSettings: "Plugin Settings", fsHideOriginal: "Hide Stock Button(Spotify Premium)", language: "Language", lyricsHeader: "Lyrics Settings", lyrics: "Lyrics", lyricsAlignment: { setting: "Lyrics Alignment", left: "Left", center: "Center", right: "Right", }, lyricsAnimationTempo: "Lyrics Animation Tempo", generalHeader: "General Settings", progressBar: "Progress Bar", playerControls: "Player Controls", trimTitle: "Trim Title", trimTitleUpNext: "Trim Title(Up Next)", showAlbum: { setting: "Show Album", never: "Never", always: "Always", date: "Show with Release Date", }, showAllArtists: "Show All Artists", icons: "Icons", songChangeAnimation: "Song Change Animation", fullscreen: "Fullscreen", extraHeader: "Extra Functionality", backgroundChoice: { setting: "Background choice", color: "Solid color", artwork: "Artwork", }, extraControls: "Extra Controls", upnextDisplay: "Upnext Display", contextDisplay: { setting: "Context Display", always: "Always", never: "Never", mouse: "On mouse movement", }, volumeDisplay: { setting: "Volume Bar Display", always: "Always", never: "Never", mouse: "On mouse movement", volume: "On volume change", }, appearanceHeader: "Advanced/Appearance", appearanceSubHeader: "Only change if you know what you are doing!", backgroundColor: { setting: "Color Choice on Colored Background", vibrant: "Vibrant", prominent: "Prominent", desaturated: "Desaturated (recommended)", lightVibrant: "Light Vibrant", darkVibrant: "Dark Vibrant", vibrantNonAlarming: "Vibrant Non Alarming", }, themedButtons: "Themed Buttons", themedIcons: "Themed Icons", invertColors: { setting: "Invert Colors", never: "Never", always: "Always", auto: "Automatic (Based on BG)", }, backAnimationTime: "Background Animation Time", upnextScroll: { setting: "Upnext Scroll Animation", mq: "Marquee/Scrolling", sp: "Spotify/Translating", }, upnextTime: "Upnext Time to Show", backgroundBlur: "Background Blur", backgroundBrightness: "Background Brightness", cgfReset: "Reset Config", reload: "Reload Client", }, tvBtnDesc: "TV Mode Display", fullscreenBtnDesc: "Full Screen", }, "it-IT": { langName: "Italiano", context: { queue: "Riproduzione da coda", track: "Riproduzione brano", album: "Riproduzione album", artist: "Riproduzione artista", playlist: "Riproduzione playlist", playlistFolder: "Riproduzione da cartella playlist", search: "Riproduzione da ricerca", searchDest: "Brani", collection: "Riproduzione dalla libreria", likedSongs: "Brani che ti piacciono", trackRadio: "Radio dal brano", artistRadio: "Radio da artista", albumRadio: "Radio da album", playlistRadio: "Radio da playlist", }, upnext: "In coda", unknownArtist: "Artista sconosciuto", settings: { switchToTV: "Passa a modalità TV", switchToFullscreen: "Passa a modalità fullscreen", tvModeConfig: "Modalità TV", fullscreenConfig: "Modalità fullscreen", exit: "Esci", pluginSettings: "Impostazioni plugin", fsHideOriginal: "Pulsante Nascondi Stock (Spotify Premium)", language: "Lingua", lyricsHeader: "Impostazioni testo", lyrics: "Mostra testo", lyricsAlignment: { setting: "Allineamento testo", left: "Sinistra", center: "Centro", right: "Destra", }, lyricsAnimationTempo: "Durata animazione testo", generalHeader: "Impostazioni generali", progressBar: "Barra di avanzamento", playerControls: "Controlli player", trimTitle: "Accorcia titolo", trimTitleUpNext: "Accorcia titolo in Up next", showAlbum: { setting: "Mostra album", never: "Mai", always: "Sempre", date: "Mostra con data di uscita", }, showAllArtists: "Mostra tutti gli artisti", icons: "Icone", songChangeAnimation: "Animazione cambio brano", fullscreen: "Schermo intero", extraHeader: "Funzionalità aggiuntive", backgroundChoice: { setting: "Scelta sfondo", color: "Tinta unita", artwork: "Artwork", }, extraControls: "Controlli aggiuntivi", upnextDisplay: "Mostra Up next", contextDisplay: { setting: "Mostra contesto", always: "Sempre", never: "Mai", mouse: "Al movimento del mouse", }, volumeDisplay: { setting: "Mostra barra volume", always: "Sempre", never: "Mai", mouse: "Al movimento del mouse", volume: "Al cambio volume", }, appearanceHeader: "Avanzato/Aspetto", appearanceSubHeader: "Cambia solo se sai cosa stai facendo!", backgroundColor: { setting: "Colore su sfondo tinta unita", vibrant: "Vibrant", prominent: "Prominent", desaturated: "Desaturato (raccomandato)", lightVibrant: "Vibrant chiaro", darkVibrant: "Vibrant scuro", vibrantNonAlarming: "Vibrant delicato", }, themedButtons: "Pulsanti a tema", themedIcons: "Icone a tema", invertColors: { setting: "Inverti colori", never: "Mai", always: "Sempre", auto: "Automatico (basato su sfondo)", }, backAnimationTime: "Durata animazione sfondo", upnextScroll: { setting: "Scorrimento Up next", mq: "Scorrimento", sp: "Traslazione", }, upnextTime: "Tempo per mostrare Up next", backgroundBlur: "Sfocamento sfondo", backgroundBrightness: "Luminosità sfondo", cgfReset: "Resetta configurazione", reload: "Ricarica client", }, tvBtnDesc: "Modalità TV", fullscreenBtnDesc: "Schermo intero", }, "zh-CN": { langName: "简体中文", context: { queue: "正在从队列播放", track: "正在播放", album: "正在从专辑播放", artist: "正在从艺人播放", playlist: "正在从歌单播放", playlistFolder: "正在从歌单文件夹播放", search: "正在从搜索结果播放", searchDest: "歌曲", collection: "正在从合辑播放", likedSongs: "已点赞的歌曲", trackRadio: "正在从歌曲电台播放", artistRadio: "正在从艺人电台播放", albumRadio: "正在从专辑电台播放", playlistRadio: "正在从歌单电台播放", }, upnext: "下一首", unknownArtist: "未知歌手", settings: { switchToTV: "切换至电视模式", switchToFullscreen: "切换至默认模式", tvModeConfig: "电视模式配置", fullscreenConfig: "全屏模式配置", exit: "退出", pluginSettings: "插件设置", fsHideOriginal: "隐藏切换至原版按钮 (Spotify Premium)", language: "语言", lyricsHeader: "歌词设置", lyrics: "歌词", lyricsAlignment: { setting: "歌词对齐方式", left: "靠左", center: "居中", right: "靠右", }, lyricsAnimationTempo: "歌词动画速度", generalHeader: "通用设置", progressBar: "播放进度条", playerControls: "播放控制", trimTitle: "缩短标题", trimTitleUpNext: "缩短标题 (下一首提示)", showAlbum: { setting: "显示标题", never: "永不", always: "总是", date: "与发布日期一起显示", }, showAllArtists: "显示所有艺人", icons: "显示图标", songChangeAnimation: "切歌动画", fullscreen: "全屏", extraHeader: "额外功能", backgroundChoice: { setting: "背景", color: "纯色", artwork: "艺术画", }, extraControls: "额外控件", upnextDisplay: "下一首提示", contextDisplay: { setting: "内容来源显示", always: "总是", never: "从不", mouse: "当鼠标移动时", }, volumeDisplay: { setting: "音量显示", always: "总是", never: "从不", mouse: "当鼠标移动时", volume: "当音量调节时", }, appearanceHeader: "高级/外观", appearanceSubHeader: "不要动除非你知道自己在做什么", backgroundColor: { setting: "纯色背景的颜色配置", vibrant: "Vibrant", prominent: "Prominent", desaturated: "Desaturated (推荐)", lightVibrant: "Light Vibrant", darkVibrant: "Dark Vibrant", vibrantNonAlarming: "Vibrant Non Alarming", }, themedButtons: "主题色按钮", themedIcons: "主题色图标", invertColors: { setting: "反转颜色", never: "从不", always: "总是", auto: "自动(基于背景)", }, backAnimationTime: "背景动画时间", upnextScroll: { setting: "下一首提示滚动", mq: "Marquee/滚动", sp: "Spotify/变换", }, upnextTime: "下一首提示显示时间", backgroundBlur: "背景模糊", backgroundBrightness: "背景亮度", cgfReset: "重置配置", reload: "重新载入应用", }, tvBtnDesc: "电视模式显示", fullscreenBtnDesc: "全屏", }, }; let INIT_RETRIES = 0; function fullScreen() { const extraBar = document.querySelector(".main-nowPlayingBar-right")?.childNodes[0] || document.querySelector(".ExtraControls") || document.querySelector(".ClYTTKGdd9KB7D9MXicj"); const topBar = document.querySelector(".main-topBar-historyButtons"); const { CosmosAsync, Keyboard, Player, Platform } = Spicetify; let entriesToVerify = [topBar, extraBar, CosmosAsync, Keyboard, Player, Platform]; if (INIT_RETRIES > 50) { entriesToVerify.forEach((entry) => { if (!entry) { console.error("Spicetify method not available. Report issue on GitHub or run Spicetify.test() to test."); Spicetify.showNotification(`Error initializing "fullscreen.js" extension. Spicetify method not available. Report issue on GitHub.`); } }); return; } if (entriesToVerify.some((it) => !it)) { setTimeout(fullScreen, 300); INIT_RETRIES += 1; return; } Spicetify.Keyboard.registerShortcut( { key: Spicetify.Keyboard.KEYS["T"], ctrl: false, shift: false, alt: false, }, openwithTV ); Spicetify.Keyboard.registerShortcut( { key: Spicetify.Keyboard.KEYS["F"], ctrl: false, shift: false, alt: false, }, openwithDef ); function openwithTV() { if (!document.body.classList.contains("fsd-activated") || !CONFIG.tvMode || ACTIVE !== "tv") { if (!CONFIG.tvMode || ACTIVE !== "tv") { CONFIG["tvMode"] = true; ACTIVE = "tv"; saveConfig(); render(); } activate(); } else deactivate(); } function openwithDef() { if (!document.body.classList.contains("fsd-activated") || CONFIG.tvMode || ACTIVE !== "def") { if (CONFIG.tvMode || ACTIVE !== "def") { CONFIG["tvMode"] = false; ACTIVE = "def"; saveConfig(); render(); } activate(); } else deactivate(); } const DEFAULTS = { tv: { lyricsDisplay: true, lyricsAlignment: "right", animationTempo: 0.2, progressBarDisplay: false, playerControls: false, trimTitle: true, trimTitleUpNext: true, showAlbum: "d", showAllArtists: true, icons: true, titleMovingIcon: false, enableFade: true, enableFullscreen: true, extraControls: false, upnextDisplay: true, contextDisplay: "a", volumeDisplay: "o", themedButtons: true, themedIcons: true, invertColors: "n", backAnimationTime: 0.4, upNextAnim: "sp", upnextTimeToShow: 45, blurSize: 0, backgroundBrightness: 0.4, }, def: { lyricsDisplay: true, lyricsAlignment: "right", animationTempo: 0.2, progressBarDisplay: true, playerControls: true, trimTitle: true, trimTitleUpNext: true, showAlbum: "n", showAllArtists: true, icons: false, titleMovingIcon: false, enableFade: true, enableFullscreen: true, backgroundChoice: "a", extraControls: true, upnextDisplay: true, contextDisplay: "m", volumeDisplay: "o", themedButtons: true, themedIcons: false, invertColors: "n", backAnimationTime: 1, upNextAnim: "sp", upnextTimeToShow: 30, coloredBackChoice: "DESATURATED", blurSize: 24, backgroundBrightness: 0.7, }, tvMode: false, locale: "en-US", fsHideOriginal: false, }; const CONFIG = getConfig(); if (localStorage.getItem("full-screen:inverted") === null) { localStorage.setItem("full-screen:inverted", "{}"); } const INVERTED = JSON.parse(localStorage.getItem("full-screen:inverted") ?? "{}"); let ACTIVE = CONFIG.tvMode ? "tv" : "def"; let LOCALE = CONFIG.locale; const OFFLINESVG = `data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCI+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiB4PSIwIiB5PSIwIiAvPgogIDxwYXRoIGZpbGw9IiNCM0IzQjMiIGQ9Ik0yNi4yNSAxNi4xNjJMMjEuMDA1IDEzLjEzNEwyMS4wMTIgMjIuNTA2QzIwLjU5NCAyMi4xOTIgMjAuMDgxIDIxLjk5OSAxOS41MTkgMjEuOTk5QzE4LjE0MSAyMS45OTkgMTcuMDE5IDIzLjEyMSAxNy4wMTkgMjQuNDk5QzE3LjAxOSAyNS44NzggMTguMTQxIDI2Ljk5OSAxOS41MTkgMjYuOTk5QzIwLjg5NyAyNi45OTkgMjIuMDE5IDI1Ljg3OCAyMi4wMTkgMjQuNDk5QzIyLjAxOSAyNC40MjIgMjIuMDA2IDE0Ljg2NyAyMi4wMDYgMTQuODY3TDI1Ljc1IDE3LjAyOUwyNi4yNSAxNi4xNjJaTTE5LjUxOSAyNS45OThDMTguNjkyIDI1Ljk5OCAxOC4wMTkgMjUuMzI1IDE4LjAxOSAyNC40OThDMTguMDE5IDIzLjY3MSAxOC42OTIgMjIuOTk4IDE5LjUxOSAyMi45OThDMjAuMzQ2IDIyLjk5OCAyMS4wMTkgMjMuNjcxIDIxLjAxOSAyNC40OThDMjEuMDE5IDI1LjMyNSAyMC4zNDYgMjUuOTk4IDE5LjUxOSAyNS45OThaIi8+Cjwvc3ZnPgo=`; const style = document.createElement("style"); const container = document.createElement("div"); container.id = "full-screen-display"; container.classList.add("Video", "VideoPlayer--fullscreen", "VideoPlayer--landscape"); let cover, back, title, artist, album, prog, elaps, durr, play, ctx_container, ctx_icon, ctx_source, ctx_name, fsd_myUp, fsd_nextCover, fsd_up_next_text, fsd_next_tit_art, fsd_next_tit_art_inner, fsd_first_span, fsd_second_span, volumeContainer, volumeCurr, volumeBarInner, volumeIcon, playingIcon, pausedIcon, nextControl, backControl, heart, shuffle, repeat, invertButton, lyrics; const nextTrackImg = new Image(); const artistImg = new Image(); function render() { container.classList.toggle("lyrics-active", !!CONFIG[ACTIVE].lyricsDisplay); if (!CONFIG[ACTIVE].lyricsDisplay || !CONFIG[ACTIVE].extraControls) container.classList.remove("lyrics-hide-force"); const styleBase = ` #full-screen-display { display: none; z-index: 100; position: fixed; width: 100%; height: 100%; cursor: default; left: 0; top: 0; --transition-duration: .8s; --transition-function: ease-in-out; --main-color: 255,255,255; --contrast-color: 0,0,0; --primary-color: rgba(var(--main-color),1); --secondary-color: rgba(var(--main-color),.7); --tertiary-color: rgba(var(--main-color),.5); --theme-color: 175,175,175; --theme-background-color: rgba(175,175,175,.6); --theme-hover-color: rgba(175,175,175,.3); --theme-main-color: rgba(var(--theme-color),1); } #full-screen-display.themed-buttons{ --theme-background-color: rgba(var(--theme-color),.6); --theme-hover-color: rgba(var(--theme-color),.3); } .unavailable{ color: var(--tertiary-color) !important; pointer-events: none !important; opacity: .5 !important; background: transparent !important; } /* .unavailable::after{ content: ""; display: block; background-color: #FFF; bottom: 50%; left: 0; right: 0; position: absolute; transform: rotateZ(45deg); width: 25px; height: 1.5px; } .unavailable::before{ content: ""; display: block; background-color: #FFF; bottom: 50%; left: 0; right: 0; position: absolute; transform: rotateZ(-45deg); width: 25px; height: 1.5px; } */ @keyframes fadeUp{ 0%{ opacity:0; transform:translateY(-10px) } to{ opacity:1; transform:translateY(0) } } @keyframes fadeDo{ 0%{ opacity:0; transform:translateY(10px) } to{ opacity:1; transform:translateY(0) } } @keyframes fadeRi{ 0%{ opacity:0; transform:translateX(10px) } to{ opacity:1; transform:translateX(0) } } @keyframes fadeLe{ 0%{ opacity:0; transform:translateX(-10px) } to{ opacity:1; transform:translateX(0) } } .fade-do{ animation: fadeDo .5s cubic-bezier(.3, 0, 0, 1); } .fade-up{ animation: fadeUp .5s cubic-bezier(.3, 0, 0, 1); } .fade-ri{ animation: fadeRi .5s cubic-bezier(.3, 0, 0, 1); } .fade-le{ animation: fadeLe .5s cubic-bezier(.3, 0, 0, 1); } button.dot-after{ padding-bottom: 3px !important; } .dot-after:after{ background-color: currentColor; border-radius: 50%; bottom: 3px; content: ""; display: block; height: 4px; left: 50%; position: absolute; transform: translateX(-50%); width: 4px; } #fsd-ctx-container { background-color: transparent; color: var(--secondary-color); position: fixed; float: left; top: 30px; left: 50px; display: flex; flex-direction: row; justify-content: center; align-items: center; text-align: left; z-index: 50; transition: all 1s ease-in-out; opacity: 1; max-width: 40%; } #fsd-ctx-details{ padding-left: 18px; line-height: initial; font-size: 18px; overflow: hidden; } #fsd-ctx-icon{ width: 48px; height : 48px; } #fsd-ctx-icon svg{ fill: var(--primary-color) !important; } #fsd-ctx-source{ text-transform: uppercase; } #fsd-ctx-name{ font-weight: 700; font-size: 20px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } .ctx-no-name{ padding-bottom: 2px; font-size: 24px; font-weight: 600; } #fsd-upnext-container{ float: right; width: 472px; height: 102px; max-width: 45%; position: fixed; top: 45px; right: 60px; display: flex; border: 1px solid rgba(130,130,130,.7); border-radius: 10px; background-color: rgba(20,20,20,1); flex-direction: row; text-align: left; z-index: 50; transition: transform .8s ease-in-out; transform: translateX(600px); } #fsd_next_art_image{ background-size: cover; background-position: center; width: 100px; height: 100px; border-radius: 9px 0 0 9px; } #fsd_next_details{ padding-left: 18px; padding-top: 17px; line-height: initial; width: calc(100% - 115px); color: rgba(255,255,255,1); font-size: 19px; overflow: hidden; } #fsd_next_tit_art{ padding-top: 9px; font-size: 22px; font-weight: 700; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } @keyframes fsd_cssmarquee { 0% { transform: translateX(0%); } 18% { transform: translateX(0%); } 100% { transform: translateX(var(--translate_width_fsd)); } } @keyframes fsd_translate { 0%,10% { transform: translateX(0%); } 50%,55% { transform: translateX(var(--translate_width_fsd)); } 100% { transform: translateX(0%); } } #fsd-volume-container{ position: fixed; text-align: center; background-color: transparent; color: var(--primary-color); float: left; top: 30%; left: 30px; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 50; height: 200px; max-height: 33vh; transition: transform .8s var(--transition-function); } #fsd-volume-container.v-hidden{ transform: translateX(-100px) scale(.1); } #fsd-volume-container:hover{ transform: translateX(0px) scale(1); } #fsd-volume-bar{ margin: 8px 0; border-radius: 4px; background-color: rgba(var(--main-color),.35); overflow: hidden; width: 8px; height: 100%; display: flex; align-items: end; } #fsd-volume-bar-inner{ width: 100%; border-radius: 4px; background-color: var(--primary-color); box-shadow: 4px 0 12px rgba(0, 0, 0, 0.8); transition: height .2s var(--transition-function); } #fsd-volume-icon svg{ fill: var(--primary-color) !important; } .unavailable #fsd-volume-bar-inner{ height: 100%; background-color: var(--tertiary-color); } #fsd-volume{ width: 50px; font-size: 18px; } #fad-lyrics-plus-container{ transition: transform var(--transition-duration) var(--transition-function); position: absolute; right: -50px; max-width: 50%; top: 7.5vh; } .lyrics-unavailable #fad-lyrics-plus-container, .lyrics-hide-force #fad-lyrics-plus-container{ transform: translateX(1000px) scale3d(.1,.1,.1) rotate(45deg); } #fad-lyrics-plus-container .lyrics-lyricsContainer-LyricsContainer{ --lyrics-color-active: var(--primary-color) !important; --lyrics-color-inactive: var(--tertiary-color) !important; --lyrics-highlight-background: rgba(var(--contrast-color),.7) !important; --lyrics-align-text: ${CONFIG[ACTIVE].lyricsAlignment} !important; --animation-tempo: ${CONFIG[ACTIVE].animationTempo}s !important; height: 85vh !important; } .lyrics-config-button{ margin-right: 20px; } #fsd-foreground { position: relative; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: var(--primary-color); transition: all var(--transition-duration) var(--transition-function); } #fsd-art-image { position: relative; width: 100%; height: 100%; padding-bottom: 100%; border-radius: 8px; background-size: cover; } #fsd-art-inner { position: absolute; left: 3%; bottom: 0; width: 94%; height: 94%; z-index: -1; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6) !important; transform: translateZ(0); } #fsd-artist{ font-weight: 500; color: var(--secondary-color); } #fsd-album{ font-weight: 400; color: var(--tertiary-color); } .fsd-controls{ display: flex; flex-direction: row; column-gap: 10px; } #fsd-progress { width: 100%; height: 6px; border-radius: 4px; background-color: rgba(var(--main-color),.35); overflow: hidden; } #fsd-progress-inner { height: 100%; border-radius: 4px; background-color: var(--primary-color); box-shadow: 4px 0 12px rgba(0, 0, 0, 0.8); } #fsd-elapsed, #fsd-duration{ min-width: 35px; text-align: center; } #fsd-elapsed { margin-right: 10px; } #fsd-duration { margin-left: 10px; } #fsd-background { position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: -2; } #full-screen-display .fs-button{ background: transparent; border: 0; border-radius: 8px; color: var(--primary-color); padding: 3px 5px 0 5px; cursor: pointer; position: relative; transition: all .3s var(--transition-function), transform .1s var(--transition-function); } #full-screen-display .fs-button:hover{ transform: scale(1.2); filter: saturate(1.5) contrast(1.5) !important; background: var(--theme-hover-color); } #full-screen-display .fs-button.button-active{ background: var(--theme-background-color) !important; filter: saturate(1.5) contrast(1.5) !important; } #fsd-foreground svg{ fill: var(--primary-color); transition: all .3s var(--transition-function); } .themed-icons #fsd-foreground svg{ fill: var(--theme-main-color); filter: saturate(1.8); } .themed-icons.themed-buttons .fs-button.button-active svg{ fill: var(--primary-color) !important; } .fsd-background-fade { transition: background-image var(--fs-transition) linear; } body.fsd-activated #full-screen-display { display: block; } .fsd-activated .Root__top-bar, .fsd-activated .Root__nav-bar, .fsd-activated .Root__main-view, .fsd-activated .Root__now-playing-bar{ visibility: hidden; display: none; } .main-notificationBubbleContainer-NotificationBubbleContainer{ z-index: 1000; }`; const styleChoices = [ ` #full-screen-display, #full-screen-display.lyrics-unavailable, #full-screen-display.lyrics-hide-force{ --fsd-foreground-transform: 50%; --fsd-art-max-width: 600px; --fsd-items-max-width: 580px; --fsd-title-size: 50px; --fsd-sec-size: 28px; } #full-screen-display.lyrics-active :not(#full-screen-display.lyrics-unavailable *,#full-screen-display.lyrics-hide-force *){ --fsd-foreground-transform: 0px; --fsd-art-max-width: 500px; --fsd-items-max-width: 480px; --fsd-title-size: 40px; --fsd-sec-size: 23px; } #fsd-art, #fsd-details, #fsd-status, #fsd-progress-container{ transition: all var(--transition-duration) var(--transition-function); } #fsd-foreground { transform: translateX(var(--fsd-foreground-transform)); width: 50%; flex-direction: column; text-align: center; } #fsd-art { width: calc(100vh - 300px); max-width: var(--fsd-art-max-width); min-width: 300px; } #fsd-title{ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } #fsd-album, #fsd-artist{ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } #fsd-progress-container { width: 28vw; max-width: var(--fsd-items-max-width); display: flex; align-items: center; } #fsd-details { padding-top: 30px; line-height: initial; max-width: var(--fsd-items-max-width); color: var(--primary-color); } #fsd-status { display: flex; width: 28vw; max-width: var(--fsd-items-max-width); align-items: center; justify-content: space-between; flex-direction: row; } #fsd-status.active { margin: 5px auto 0; gap: 10px; } #fsd-title { font-size: var(--fsd-title-size); font-weight: 900; transition: all var(--transition-duration) var(--transition-function); } #fsd-artist,#fsd-album{ font-size: var(--fsd-sec-size); transition: all var(--transition-duration) var(--transition-function); } @media (max-width: 900px), (max-height: 900px){ #fsd-title{ font-size: 35px; font-weight: 600; } } #fsd-title svg{ width: 35px; height: 35px; } .lyrics-active #fsd-title svg{ width: 30px; height: 30px; } .lyrics-unavailable #fsd-title svg,.lyrics-hide-force #fsd-title svg{ width: 35px; height: 35px; } #playing-icon{ width: 28px !important; height: 28px !important; margin-right: 7px; } .lyrics-active #playing-icon{ margin-right: 2px; } .lyrics-unavailable #playing-icon,.lyrics-hide-force #playing-icon{ margin-right: 7px; } #fsd-artist svg, #fsd-album svg{ width: calc(var(--fsd-sec-size) - 6px); height: calc(var(--fsd-sec-size) - 6px); margin-right: 5px; } .fsd-controls { margin-top: 10px; margin-bottom: 5px; } .fsd-controls-left{ width: 30%; justify-content: flex-start; } .fsd-controls-center{ width: 40%; justify-content: center; margin: 10px auto 5px; } .fsd-controls-right{ width: 30%; justify-content: flex-end; } `, `#fsd-background-image { height: 100%; background-size: cover; filter: brightness(${CONFIG[ACTIVE].backgroundBrightness}) blur(${CONFIG[ACTIVE].blurSize}px); background-position: center; transform: translateZ(0); } #fsd-foreground { flex-direction: row; text-align: left; justify-content: left; align-items: flex-end; position: absolute; top: auto; bottom: 75px; } .lyrics-active #fsd-foreground{ width: max-content; max-width: 65%; } #fsd-art { width: calc(100vw - 840px); min-width: 180px; max-width: 220px; margin-left: 65px; } #fsd-progress-container { width: 100%; max-width: 450px; display: flex; align-items: center; } .fsd-controls + #fsd-progress-container{ padding-left: 10px; } #fsd-details { padding-left: 30px; line-height: initial; width: 80%; color: var(--primary-color); } #fsd-title, #fsd-album, #fsd-artist{ display: flex; justify-content: flex-start; align-items: baseline; gap: 5px; } #fsd-title span, #fsd-album span, #fsd-artist span{ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } #fsd-title span{ -webkit-line-clamp: 3; } #fsd-title svg, #fsd-artist svg, #fsd-album svg{ flex: 0 0 auto; } #fsd-album{ position: relative; } #fsd-album svg{ position: absolute; top: 7px; } #fsd-album svg + span{ margin-left: 40px; } #fsd-title { font-size: 62px; font-weight: 900; } @media (max-width: 900px), (max-height: 800px){ #fsd-title{ font-size: 40px; font-weight: 600; } #fsd-artist, #fsd-album { font-size: 20px; } } #fsd-artist, #fsd-album { font-size: 28px; } #fsd-title svg{ width: 35px; height: 45px; } #playing-icon{ width: 30px !important; height: 40px !important; margin-right: 5px; } #fsd-artist svg, #fsd-album svg { margin-right: 15px; width: 22px; height: 22px; } #fsd-status { display: flex; flex-direction: row; min-width: 450px; max-width: 450px; align-items: center; justify-content: space-between; } #fsd-status.active { column-gap: 10px; margin: 10px 0; } `, ]; const iconStyleChoices = [ ` #fsd-title svg, #fsd-artist svg, #fsd-album svg { display: none; }`, ` #fsd-title svg, #fsd-artist svg, #fsd-album svg { transition: all var(--transition-duration) var(--transition-function); display: inline-block; }`, ]; Spicetify.Player.removeEventListener("songchange", updateInfo); if (progressListener) clearInterval(progressListener); Spicetify.Player.removeEventListener("onplaypause", updatePlayerControls); Spicetify.Player.removeEventListener("onplaypause", updatePlayingIcon); Spicetify.Player.origin._events.removeListener("update", updateExtraControls); heartObserver.disconnect(); Spicetify.Player.origin._events.removeListener("queue_update", updateUpNext); Spicetify.Player.origin._events.removeListener("update", updateUpNextShow); window.removeEventListener("resize", updateUpNext); upNextShown = false; if (Spicetify.Platform?.PlaybackAPI === undefined) Spicetify.Player.origin._events.removeListener("volume", updateVolume); else Spicetify.Platform.PlaybackAPI._events.removeListener("volume", updateVolume); if (origLoc !== "/lyrics-plus" && document.body.classList.contains("fsd-activated")) { Spicetify.Platform.History.push(origLoc); Spicetify.Platform.History.entries.splice(Spicetify.Platform.History.entries.length - 3, 2); Spicetify.Platform.History.index = Spicetify.Platform.History.index > 0 ? Spicetify.Platform.History.index - 2 : -1; Spicetify.Platform.History.length = Spicetify.Platform.History.length > 1 ? Spicetify.Platform.History.length - 2 : 0; } window.dispatchEvent(new Event("fad-request")); window.removeEventListener("lyrics-plus-update", handleLyricsUpdate); container.removeEventListener("mousemove", hideCursor); container.removeEventListener("mousemove", hideContext); container.removeEventListener("mousemove", hideVolume); if (curTimer) clearTimeout(curTimer); if (ctxTimer) clearTimeout(ctxTimer); if (volTimer) clearTimeout(volTimer); style.innerHTML = styleBase + styleChoices[CONFIG.tvMode ? 1 : 0] + iconStyleChoices[CONFIG[ACTIVE].icons ? 1 : 0]; container.innerHTML = ` ${ CONFIG.tvMode ? `
` : `` } ${ CONFIG[ACTIVE].contextDisplay !== "n" ? `
` : "" } ${ CONFIG[ACTIVE].upnextDisplay ? `
` : "" } ${ CONFIG[ACTIVE].volumeDisplay !== "n" ? `
` : "" } ${CONFIG[ACTIVE].lyricsDisplay ? `
` : ""}
${Spicetify.SVGIcons.artist}
${ CONFIG[ACTIVE].showAlbum !== "n" ? `
${CONFIG[ACTIVE].icons ? `${Spicetify.SVGIcons.album}` : ""}
` : "" }
${ CONFIG[ACTIVE].extraControls ? `
` : "" } ${ CONFIG[ACTIVE].playerControls ? `
` : "" } ${ CONFIG[ACTIVE].extraControls ? `
${ CONFIG[ACTIVE].invertColors === "d" ? `` : "" } ${ CONFIG[ACTIVE].lyricsDisplay ? `` : "" }
` : "" } ${ CONFIG.tvMode && !(CONFIG[ACTIVE].playerControls && CONFIG[ACTIVE].extraControls) ? `${ CONFIG[ACTIVE].progressBarDisplay ? `
` : "" }` : "" }
${ CONFIG.tvMode && CONFIG[ACTIVE].playerControls && CONFIG[ACTIVE].extraControls ? `${ CONFIG[ACTIVE].progressBarDisplay ? `
` : "" }` : "" }
${ CONFIG.tvMode ? "" : `${ CONFIG[ACTIVE].progressBarDisplay ? `
` : "" }` }
`; if (CONFIG.tvMode) back = container.querySelector("#fsd-background-image"); else { back = container.querySelector("canvas"); back && (back.width = window.innerWidth); back && (back.height = window.innerHeight); } cover = container.querySelector("#fsd-art-image"); title = container.querySelector("#fsd-title span"); artist = container.querySelector("#fsd-artist span"); album = container.querySelector("#fsd-album span"); if (CONFIG[ACTIVE].contextDisplay !== "n") { ctx_container = container.querySelector("#fsd-ctx-container"); ctx_icon = container.querySelector("#fsd-ctx-icon"); ctx_source = container.querySelector("#fsd-ctx-source"); ctx_name = container.querySelector("#fsd-ctx-name"); } if (CONFIG[ACTIVE].upnextDisplay) { fsd_myUp = container.querySelector("#fsd-upnext-container"); fsd_myUp.onclick = Spicetify.Player.next; fsd_nextCover = container.querySelector("#fsd_next_art_image"); fsd_up_next_text = container.querySelector("#fsd_up_next_text"); fsd_next_tit_art = container.querySelector("#fsd_next_tit_art"); fsd_next_tit_art_inner = container.querySelector("#fsd_next_tit_art_inner"); fsd_first_span = container.querySelector("#fsd_first_span"); fsd_second_span = container.querySelector("#fsd_second_span"); } if (CONFIG[ACTIVE].volumeDisplay !== "n") { volumeContainer = container.querySelector("#fsd-volume-container"); volumeCurr = container.querySelector("#fsd-volume"); volumeBarInner = container.querySelector("#fsd-volume-bar-inner"); volumeIcon = container.querySelector("#fsd-volume-icon"); volumeIcon && (volumeIcon.onclick = Spicetify.Player.toggleMute); } if (CONFIG[ACTIVE].progressBarDisplay) { prog = container.querySelector("#fsd-progress-inner"); durr = container.querySelector("#fsd-duration"); elaps = container.querySelector("#fsd-elapsed"); } if (CONFIG[ACTIVE].icons) { playingIcon = container.querySelector("#playing-icon"); //Clicking on playing icon disables it and remembers the config playingIcon && (playingIcon.onclick = () => { CONFIG[ACTIVE]["titleMovingIcon"] = false; saveConfig(); playingIcon.classList.add("hidden"); pausedIcon.classList.remove("hidden"); }); pausedIcon = container.querySelector("#paused-icon"); pausedIcon && (pausedIcon.onclick = () => { CONFIG[ACTIVE]["titleMovingIcon"] = true; saveConfig(); playingIcon.classList.remove("hidden"); pausedIcon.classList.add("hidden"); updatePlayingIcon({ data: { is_paused: !Spicetify.Player.isPlaying() } }); }); } if (CONFIG[ACTIVE].playerControls) { play = container.querySelector("#fsd-play"); play && (play.onclick = () => { fadeAnimation(play); Spicetify.Player.togglePlay(); }); nextControl = container.querySelector("#fsd-next"); nextControl && (nextControl.onclick = () => { fadeAnimation(nextControl, "fade-ri"); Spicetify.Player.next(); }); backControl = container.querySelector("#fsd-back"); backControl && (backControl.onclick = () => { fadeAnimation(backControl, "fade-le"); Spicetify.Player.back(); }); } if (CONFIG[ACTIVE].extraControls) { heart = container.querySelector("#fsd-heart"); shuffle = container.querySelector("#fsd-shuffle"); repeat = container.querySelector("#fsd-repeat"); heart && (heart.onclick = () => { fadeAnimation(heart); Spicetify.Player.toggleHeart(); }); shuffle && (shuffle.onclick = () => { fadeAnimation(shuffle); Spicetify.Player.toggleShuffle(); }); repeat && (repeat.onclick = () => { fadeAnimation(repeat); Spicetify.Player.toggleRepeat(); }); if (CONFIG[ACTIVE].invertColors === "d") { invertButton = container.querySelector("#fsd-invert"); invertButton && (invertButton.onclick = () => { fadeAnimation(invertButton); if (invertButton.classList.contains("button-active")) invertButton.innerHTML = ``; else invertButton.innerHTML = ``; invertButton.classList.toggle("button-active"); if (getComputedStyle(container).getPropertyValue("--main-color").startsWith("0")) { container.style.setProperty("--main-color", "255,255,255"); container.style.setProperty("--contrast-color", "0,0,0"); if (!CONFIG.tvMode && CONFIG.def.backgroundChoice === "a") INVERTED[Spicetify.Player.data.track?.metadata?.album_uri?.split(":")[2]] = false; } else { container.style.setProperty("--main-color", "0,0,0"); container.style.setProperty("--contrast-color", "255,255,255"); if (!CONFIG.tvMode && CONFIG.def.backgroundChoice === "a") INVERTED[Spicetify.Player.data.track?.metadata?.album_uri?.split(":")[2]] = true; } localStorage.setItem("full-screen:inverted", JSON.stringify(INVERTED)); }); } if (CONFIG[ACTIVE].lyricsDisplay) { lyrics = container.querySelector("#fsd-lyrics"); lyrics && (lyrics.onclick = () => { fadeAnimation(lyrics); container.classList.toggle("lyrics-hide-force"); lyrics.classList.toggle("button-active"); lyrics.innerHTML = container.classList.contains("lyrics-unavailable") || container.classList.contains("lyrics-hide-force") ? ` ` : ``; }); } } } const classes = ["video", "video-full-screen", "video-full-window", "video-full-screen--hide-ui", "fsd-activated"]; function fullScreenOn() { if (!document.fullscreen) document.documentElement.requestFullscreen(); } function fullScreenOff() { if (document.fullscreen) document.exitFullscreen(); } function getToken() { return Spicetify.Platform.AuthorizationAPI._tokenProvider({ preferCached: true, }).then((res) => res.accessToken); } async function getTrackInfo(id) { return fetch(`https://api.spotify.com/v1/tracks/${id}`, { headers: { Authorization: `Bearer ${await getToken()}`, }, }).then((res) => res.json()); // return Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/tracks/${id}`) } async function getAlbumInfo(id) { return fetch(`https://api.spotify.com/v1/albums/${id}`, { headers: { Authorization: `Bearer ${await getToken()}`, }, }).then((res) => res.json()); // return Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/albums/${id}`) } function getPlaylistInfo(uri) { return Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri}`); } async function getArtistInfo(id) { return fetch( `https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryArtistOverview&variables=%7B%22uri%22%3A%22spotify%3Aartist%3A${id}%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22d66221ea13998b2f81883c5187d174c8646e4041d67f5b1e103bc262d447e3a0%22%7D%7D`, { headers: { Authorization: `Bearer ${await getToken()}`, }, } ) .then((res) => res.json()) .then((res) => res.data.artist); // return Spicetify.CosmosAsync.get(`https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryArtistOverview&variables=%7B%22uri%22%3A%22spotify%3Aartist%3A${id}%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22d66221ea13998b2f81883c5187d174c8646e4041d67f5b1e103bc262d447e3a0%22%7D%7D`).then(res => res.data.artist) } async function searchArt(name) { return fetch(`https://api.spotify.com/v1/search?q="${name}"&type=artist&limit=2`, { headers: { Authorization: `Bearer ${await getToken()}`, }, }).then((res) => res.json()); // return Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/search?q="${name}"&type=artist&limit=2`) } // Add fade animation on button click function fadeAnimation(ele, anim = "fade-do") { ele.classList.remove(anim); ele.classList.add(anim); setTimeout(() => { ele.classList.remove(anim); }, 800); } // Utility function to add a observer with wait for element support function addObserver(observer, selector, options) { const ele = document.querySelector(selector); if (!ele) { setTimeout(() => { addObserver(observer, selector, options); }, 2000); return; } observer.observe(ele, options); } // Converting hex to rgb function hexToRgb(hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function (m, r, g, b) { return r + r + g + g + b + b; }); let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : null; } async function getImageAndLoad(meta) { if (meta.artist_uri == null) return meta.image_xlarge_url; let arUri = meta.artist_uri.split(":")[2]; if (meta.artist_uri.split(":")[1] === "local") { let res = await searchArt(meta.artist_name).catch((err) => console.error(err)); arUri = res ? res.artists.items[0].id : ""; } let artistInfo = await getArtistInfo(arUri).catch((err) => console.error(err)); return artistInfo?.visuals?.headerImage?.sources[0].url ?? meta.image_xlarge_url; } // Set the timeout to show upnext or hide when song ends let upnextTimer, upNextShown = false; function updateUpNextShow() { setTimeout(() => { let timetogo = getShowTime(); if (upnextTimer) { clearTimeout(upnextTimer); upnextTimer = 0; } if (timetogo < 10) { if (!upNextShown || fsd_myUp.style.transform !== "translateX(0px)") { updateUpNext(); } upNextShown = true; } else { fsd_myUp.style.transform = "translateX(600px)"; upNextShown = false; if (!Spicetify.Player.origin._state.isPaused) { upnextTimer = setTimeout(() => { updateUpNext(); upNextShown = true; }, timetogo); } } }, 100); } // Return the total time left to show the upnext timer function getShowTime() { let showBefore = CONFIG[ACTIVE].upnextTimeToShow * 1000; let dur = Spicetify.Player.data.duration; let curProg = Spicetify.Player.getProgress(); if (dur - curProg <= showBefore) return -1; else return dur - showBefore - curProg; } let colorsCache = []; async function colorExtractor(uri) { const presentInCache = colorsCache.filter(obj => obj.uri === uri) if(presentInCache.length > 0) return presentInCache[0].colors; const body = await Spicetify.CosmosAsync.get(`wg://colorextractor/v1/extract-presets?uri=${uri}&format=json`); if (body.entries && body.entries.length) { const list = {}; for (const color of body.entries[0].color_swatches) { list[color.preset] = `#${color.color.toString(16).padStart(6, "0")}`; } if(colorsCache.length > 20) colorsCache.shift(); colorsCache.push({uri, colors: list}); return list; } throw "No colors returned."; } async function updateInfo() { const meta = Spicetify.Player.data.track?.metadata; if (CONFIG[ACTIVE].contextDisplay !== "n") updateContext().catch((err) => console.error("Error getting context: ", err)); // prepare title let rawTitle = meta?.title; if (CONFIG[ACTIVE].trimTitle) { rawTitle = rawTitle .replace(/\(.+?\)/g, "") .replace(/\[.+?\]/g, "") .replace(/\s\-\s.+?$/, "") .trim(); if (!rawTitle) rawTitle = meta?.title; } // prepare artist let artistName; if (CONFIG[ACTIVE].showAllArtists) { artistName = Object.keys(meta) .filter((key) => key.startsWith("artist_name")) .sort() .map((key) => meta[key]) .join(", "); } else { artistName = meta?.artist_name; } // prepare album let albumText; if (CONFIG[ACTIVE].showAlbum !== "n") { albumText = meta?.album_title || ""; if (album) album.innerText = albumText || ""; const albumURI = meta?.album_uri; if (albumURI?.startsWith("spotify:album:") && CONFIG[ACTIVE].showAlbum === "d") { getAlbumInfo(albumURI.replace("spotify:album:", "")) .then((albumInfo) => { if (!albumInfo?.release_date) throw Error("No release Date"); const albumDate = new Date(albumInfo.release_date); const recentDate = new Date(); recentDate.setMonth(recentDate.getMonth() - 18); const dateStr = albumDate.toLocaleString( LOCALE, albumDate > recentDate ? { year: "numeric", month: "short" } : { year: "numeric" } ); albumText += " • " + dateStr.charAt(0).toUpperCase() + dateStr.slice(1); if (album) album.innerText = albumText || ""; }) .catch((err) => console.error(err)); } } // prepare duration let durationText; if (CONFIG[ACTIVE].progressBarDisplay) { durationText = Spicetify.Player.formatTime(meta?.duration); } const previousImg = nextTrackImg.cloneNode(); nextTrackImg.src = meta?.image_xlarge_url; // Wait until next track image is downloaded then update UI text and images nextTrackImg.onload = async () => { if (!CONFIG.tvMode) { updateMainColor(Spicetify.Player.data.track?.uri, meta); updateThemeColor(Spicetify.Player.data.track?.uri); if (CONFIG.def.backgroundChoice == "a") { animateCanvas(previousImg, nextTrackImg); } else { animateColor(await getNextColor()); } } cover.style.backgroundImage = `url("${nextTrackImg.src}")`; title.innerText = rawTitle || ""; artist.innerText = artistName || ""; if (durr) { durr.innerText = durationText || ""; } new Image().src = Spicetify?.Queue?.nextTracks[0]?.contextTrack?.metadata.image_xlarge_url; }; nextTrackImg.onerror = () => { // Placeholder console.error("Check your Internet! Unable to load Image"); nextTrackImg.src = OFFLINESVG; }; if (CONFIG.tvMode) { artistImg.src = await getImageAndLoad(meta); updateMainColor(artistImg.src, meta); updateThemeColor(artistImg.src); artistImg.onload = async () => { back.style.backgroundImage = `url("${artistImg.src}")`; let newurl = await getImageAndLoad(Spicetify?.Queue?.nextTracks[0]?.contextTrack?.metadata); new Image().src = newurl; }; } } async function getNextColor() { let nextColor; const imageColors = await colorExtractor(Spicetify.Player.data.track?.uri).catch((err) => console.warn(err)); if (!imageColors || !imageColors[CONFIG.def.coloredBackChoice]) nextColor = "#444444"; else nextColor = imageColors[CONFIG.def.coloredBackChoice]; return nextColor; } async function updateMainColor(imageURL, meta) { switch (CONFIG[ACTIVE].invertColors) { case "a": container.style.setProperty("--main-color", "0,0,0"); container.style.setProperty("--contrast-color", "255,255,255"); break; case "d": let mainColor, contrastColor; if (!CONFIG.tvMode && CONFIG.def.backgroundChoice === "a" && meta.album_uri.split(":")[2] in INVERTED) { mainColor = INVERTED[meta.album_uri.split(":")[2]] ? "0,0,0" : "255,255,255"; } else { let imageProminentColor; const imageColors = await colorExtractor(imageURL).catch((err) => console.warn(err)); if (CONFIG.tvMode || CONFIG.def.backgroundChoice == "a") { if (!imageColors?.PROMINENT) imageProminentColor = "0,0,0"; else imageProminentColor = hexToRgb(imageColors.PROMINENT); } else { if (!imageColors || !imageColors[CONFIG.def.coloredBackChoice]) imageProminentColor = hexToRgb("#444444"); else imageProminentColor = hexToRgb(imageColors[CONFIG.def.coloredBackChoice]); } const thresholdValue = 260 - CONFIG[ACTIVE].backgroundBrightness * 100; const isLightBG = Number(imageProminentColor?.split(",")[0]) * 0.299 + Number(imageProminentColor?.split(",")[1]) * 0.587 + Number(imageProminentColor?.split(",")[2]) * 0.114 > thresholdValue; mainColor = isLightBG && CONFIG[ACTIVE].backgroundBrightness > 0.3 ? "0,0,0" : "255,255,255"; contrastColor = isLightBG && CONFIG[ACTIVE].backgroundBrightness > 0.3 ? "255,255,255" : "0,0,0"; } container.style.setProperty("--main-color", mainColor); container.style.setProperty("--contrast-color", contrastColor ?? "0,0,0"); if (CONFIG[ACTIVE].extraControls) { invertButton.classList.remove("button-active"); invertButton.innerHTML = ``; } break; case "n": default: container.style.setProperty("--main-color", "255,255,255"); container.style.setProperty("--contrast-color", "0,0,0"); break; } } //Set main theme color for the display async function updateThemeColor(imageURL) { if ( !(!CONFIG.tvMode && CONFIG.def.backgroundChoice == "c" && CONFIG.def.coloredBackChoice == "VIBRANT") && (CONFIG[ACTIVE].themedButtons || CONFIG[ACTIVE].themedIcons) ) { container.classList.toggle("themed-buttons", !!CONFIG[ACTIVE].themedButtons); container.classList.toggle("themed-icons", !!CONFIG[ACTIVE].themedIcons); let themeVibrantColor; const artColors = await colorExtractor(imageURL).catch((err) => console.warn(err)); if (!artColors?.VIBRANT) themeVibrantColor = "175,175,175"; else themeVibrantColor = hexToRgb(artColors.VIBRANT); container.style.setProperty("--theme-color", themeVibrantColor); } else { container.classList.remove("themed-buttons", "themed-icons"); container.style.setProperty("--theme-color", "175,175,175"); } } function handleLyricsUpdate(evt) { if (evt.detail.isLoading) return; container.classList.toggle("lyrics-unavailable", !(evt.detail.available && evt.detail?.synced?.length > 1)); if (CONFIG[ACTIVE].extraControls) { lyrics.classList.toggle("hidden", container.classList.contains("lyrics-unavailable")); } } let prevColor = "#000000"; async function animateColor(nextColor) { const configTransitionTime = CONFIG[ACTIVE].backAnimationTime; const { innerWidth: width, innerHeight: height } = window; back.width = width; back.height = height; const ctx = back.getContext("2d"); if (!CONFIG[ACTIVE].enableFade) { ctx.globalAlpha = 1; ctx.fillStyle = nextColor; ctx.fillRect(0, 0, width, height); return; } let previousTimeStamp, done = false, start; const animate = (timestamp) => { if (start === undefined) start = timestamp; const elapsed = timestamp - start; if (previousTimeStamp !== timestamp) { const factor = Math.min(elapsed / (configTransitionTime * 1000), 1.0); ctx.globalAlpha = 1; ctx.fillStyle = prevColor; ctx.fillRect(0, 0, width, height); ctx.globalAlpha = Math.sin((Math.PI / 2) * factor); ctx.fillStyle = nextColor; ctx.fillRect(0, 0, width, height); if (factor === 1.0) done = true; } if (elapsed < configTransitionTime * 1000) { previousTimeStamp = timestamp; !done && requestAnimationFrame(animate); } else { prevColor = nextColor; } }; requestAnimationFrame(animate); } function animateCanvas(prevImg, nextImg) { const configTransitionTime = CONFIG[ACTIVE].backAnimationTime; const { innerWidth: width, innerHeight: height } = window; back.width = width; back.height = height; const ctx = back.getContext("2d"); ctx.imageSmoothingEnabled = false; ctx.filter = `brightness(${CONFIG[ACTIVE].backgroundBrightness}) blur(${CONFIG[ACTIVE].blurSize}px)`; const blur = CONFIG[ACTIVE].blurSize; const x = -blur * 2; let y, dim; if (width > height) { dim = width; y = x - (width - height) / 2; } else { dim = height; y = x; } const size = dim + 4 * blur; if (!CONFIG[ACTIVE].enableFade) { ctx.globalAlpha = 1; ctx.drawImage(nextImg, x, y, size, size); return; } let prevTimeStamp, start, done = false; const animate = (timestamp) => { if (start === undefined) start = timestamp; const elapsed = timestamp - start; if (prevTimeStamp !== timestamp) { const factor = Math.min(elapsed / (configTransitionTime * 1000), 1.0); ctx.globalAlpha = 1; ctx.drawImage(prevImg, x, y, size, size); ctx.globalAlpha = Math.sin((Math.PI / 2) * factor); ctx.drawImage(nextImg, x, y, size, size); if (factor === 1.0) done = true; } if (elapsed < configTransitionTime * 1000) { prevTimeStamp = timestamp; !done && requestAnimationFrame(animate); } }; requestAnimationFrame(animate); } let prevUriObj; async function getContext() { let ctxIcon = "", ctxSource, ctxName; if (Spicetify.Player.data.track?.provider === "queue") { ctxIcon = ``; ctxSource = translations[LOCALE].context.queue; ctxName = ""; } else { const uriObj = Spicetify.URI.fromString(Spicetify.Player.data.context_uri); if (JSON.stringify(uriObj) === JSON.stringify(prevUriObj) && ctxSource != undefined && ctxName != undefined) return [ctxIcon, ctxSource, ctxName]; prevUriObj = uriObj; switch (uriObj.type) { case Spicetify.URI.Type.TRACK: ctxIcon = ``; ctxSource = translations[LOCALE].context.track; await getTrackInfo(uriObj._base62Id).then((meta) => (ctxName = `${meta.name} • ${meta.artists[0].name}`)); break; case Spicetify.URI.Type.SEARCH: ctxIcon = Spicetify.SVGIcons["search-active"]; ctxSource = translations[LOCALE].context.search; ctxName = `"${uriObj.query}" in ${translations[LOCALE].context.searchDest}`; break; case Spicetify.URI.Type.COLLECTION: ctxIcon = Spicetify.SVGIcons["heart-active"]; ctxSource = translations[LOCALE].context.collection; ctxName = translations[LOCALE].context.likedSongs; break; case Spicetify.URI.Type.PLAYLIST_V2: ctxIcon = Spicetify.SVGIcons["playlist"]; ctxSource = translations[LOCALE].context.playlist; ctxName = Spicetify.Player.data.context_metadata?.context_description || ""; break; case Spicetify.URI.Type.STATION: case Spicetify.URI.Type.RADIO: ctxIcon = ``; const rType = uriObj.args[0]; switch (rType) { case "album": ctxSource = translations[LOCALE].context.albumRadio; await getAlbumInfo(uriObj.args[1]).then((meta) => (ctxName = meta.name)); break; case "track": ctxSource = translations[LOCALE].context.trackRadio; await getTrackInfo(uriObj.args[1]).then((meta) => (ctxName = `${meta.name} • ${meta.artists[0].name}`)); break; case "artist": ctxSource = translations[LOCALE].context.artistRadio; await getArtistInfo(uriObj.args[1]).then((meta) => (ctxName = meta?.profile?.name)); break; case "playlist": case "playlist-v2": ctxSource = translations[LOCALE].context.playlistRadio; ctxIcon = ``; await getPlaylistInfo("spotify:playlist:" + uriObj.args[1]).then((meta) => (ctxName = meta.playlist.name)); break; default: ctxName = ""; } break; case Spicetify.URI.Type.PLAYLIST: ctxIcon = Spicetify.SVGIcons[uriObj.type]; ctxSource = translations[LOCALE].context.playlist; ctxName = Spicetify.Player.data.context_metadata.context_description || ""; break; case Spicetify.URI.Type.ALBUM: ctxIcon = Spicetify.SVGIcons[uriObj.type]; ctxSource = translations[LOCALE].context.album; ctxName = Spicetify.Player.data.context_metadata.context_description || ""; break; case Spicetify.URI.Type.ARTIST: ctxIcon = Spicetify.SVGIcons[uriObj.type]; ctxSource = translations[LOCALE].context.artist; ctxName = Spicetify.Player.data.context_metadata.context_description || ""; break; case Spicetify.URI.Type.FOLDER: ctxIcon = Spicetify.SVGIcons["playlist-folder"]; ctxSource = translations[LOCALE].context.playlistFolder; const res = await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/rootlist`, { policy: { folder: { rows: true, link: true, name: true } }, }); for (const item of res.rows) { if (item.type === "folder" && item.link === Spicetify.Player.data.context_uri) { ctxName = item.name; break; } } break; default: ctxSource = uriObj.type; ctxName = Spicetify.Player.data?.context_metadata?.context_description || ""; } } return [ctxIcon, ctxSource, ctxName]; } // Get the context and update it async function updateContext() { let ctxIcon, ctxSource, ctxName; [ctxIcon, ctxSource, ctxName] = await getContext().catch((err) => console.error(err)); ctx_source.classList.toggle("ctx-no-name", !ctxName); //Set default icon if no icon is returned if (!ctxIcon) ctxIcon = Spicetify.SVGIcons.spotify; ctx_icon.innerHTML = /^${ctxIcon}` : ctxIcon; //Only change the DOM if context is changed if (ctx_source.innerText.toLowerCase() !== `${ctxSource}`.toLowerCase() || ctx_name.innerText.toLowerCase() !== ctxName.toLowerCase()) { ctx_source.innerText = `${ctxSource}`; ctx_name.innerText = ctxName; if (CONFIG[ACTIVE].contextDisplay === "m") hideContext(); } } function updateUpNextInfo() { fsd_up_next_text.innerText = translations[LOCALE].upnext.toUpperCase(); let metadata = {}; const queue_metadata = Spicetify.Queue.nextTracks[0]; if (queue_metadata) { metadata = queue_metadata?.contextTrack?.metadata; } else { metadata["artist_name"] = ""; metadata["title"] = ""; } let rawTitle = metadata.title; if (CONFIG[ACTIVE].trimTitleUpNext) { rawTitle = rawTitle .replace(/\(.+?\)/g, "") .replace(/\[.+?\]/g, "") .replace(/\s\-\s.+?$/, "") .trim(); if (!rawTitle) rawTitle = metadata.title; } const artistNameNext = Object.keys(metadata) .filter((key) => key.startsWith("artist_name")) .sort() .map((key) => metadata[key]) .join(", "); let next_artist; if (artistNameNext) { next_artist = artistNameNext; } else { next_artist = translations[LOCALE].unknownArtist; } const next_image = metadata.image_xlarge_url; if (next_image) { fsd_nextCover.style.backgroundImage = `url("${next_image}")`; } else { if (metadata.image_url) fsd_nextCover.style.backgroundImage = `url("${metadata.image_url}")`; else { fsd_nextCover.style.backgroundImage = `url("${OFFLINESVG}")`; } } fsd_first_span.innerText = rawTitle + " • " + next_artist; fsd_second_span.innerText = rawTitle + " • " + next_artist; } async function updateUpNext() { if ( Spicetify.Player.data.duration - Spicetify.Player.getProgress() <= CONFIG[ACTIVE].upnextTimeToShow * 1000 + 50 && Spicetify.Queue?.nextTracks[0]?.contextTrack?.metadata?.title ) { await updateUpNextInfo(); fsd_myUp.style.transform = "translateX(0px)"; upNextShown = true; let animTime; if (fsd_second_span.offsetWidth > fsd_next_tit_art.offsetWidth - 2) { switch (CONFIG[ACTIVE].upNextAnim) { case "mq": fsd_first_span.style.paddingRight = "80px"; animTime = 5000 * (fsd_first_span.offsetWidth / 400); fsd_myUp.style.setProperty("--translate_width_fsd", `-${fsd_first_span.offsetWidth + 3.5}px`); fsd_next_tit_art_inner.style.animation = "fsd_cssmarquee " + animTime + "ms linear 800ms infinite"; break; case "sp": default: fsd_first_span.style.paddingRight = "0px"; fsd_second_span.innerText = ""; animTime = (fsd_first_span.offsetWidth - fsd_next_tit_art.offsetWidth - 2) / 0.05; // animTime= 3000*(fsd_first_span.offsetWidth/fsd_next_tit_art.offsetWidth) fsd_myUp.style.setProperty("--translate_width_fsd", `-${fsd_first_span.offsetWidth - fsd_next_tit_art.offsetWidth + 5}px`); fsd_next_tit_art_inner.style.animation = `fsd_translate ${animTime > 1500 ? animTime : 1500}ms linear 800ms infinite`; break; } } else { fsd_first_span.style.paddingRight = "0px"; fsd_next_tit_art_inner.style.animation = "none"; fsd_second_span.innerText = ""; } } else { upNextShown = false; fsd_myUp.style.transform = "translateX(600px)"; fsd_first_span.style.paddingRight = "0px"; fsd_next_tit_art_inner.style.animation = "none"; fsd_second_span.innerText = ""; } } let prevVolume = Spicetify.Player?.origin?._volume?._volume ?? Spicetify.Platform?.PlaybackAPI?._volume; function updateVolume(data) { const volume = !data ? Spicetify.Player?.origin?._volume?._volume ?? Spicetify.Platform?.PlaybackAPI?._volume : data.data.volume; if (volume !== prevVolume || !data) { //Only update volume when there is a change or on initial fire prevVolume = volume; if (CONFIG[ACTIVE].volumeDisplay === "o" || CONFIG[ACTIVE].volumeDisplay === "m") { volumeContainer.classList.remove("v-hidden"); } volumeBarInner.style.height = volume * 100 + "%"; let currVol = Math.round(volume * 100) === -100 ? "%" : Math.round(volume * 100); volumeCurr.innerText = currVol + "%"; volumeContainer.classList.toggle("unavailable", typeof currVol !== "number"); if (typeof currVol !== "number" || currVol > 60) volumeIcon.innerHTML = `${Spicetify.SVGIcons["volume"]}`; else if (currVol > 30) volumeIcon.innerHTML = `${Spicetify.SVGIcons["volume-two-wave"]}`; else if (currVol > 0) volumeIcon.innerHTML = `${Spicetify.SVGIcons["volume-one-wave"]}`; else volumeIcon.innerHTML = `${Spicetify.SVGIcons["volume-off"]}`; if (CONFIG[ACTIVE].volumeDisplay === "o" || CONFIG[ACTIVE].volumeDisplay === "m") { hideVolume(); } } } function updateProgress() { const progress = Spicetify.Player.formatTime(Spicetify.Player.getProgress()); if (!Spicetify.Player.origin._state.isPaused || elaps.innerText !== progress) { prog.style.width = Spicetify.Player.getProgressPercent() * 100 + "%"; elaps.innerText = progress; } } function updatePlayingIcon({ data }) { if (data.is_paused) { pausedIcon.classList.remove("hidden"); playingIcon.classList.add("hidden"); } else { pausedIcon.classList.toggle("hidden", CONFIG[ACTIVE].titleMovingIcon); playingIcon.classList.toggle("hidden", !CONFIG[ACTIVE].titleMovingIcon); } } function updatePlayerControls({ data }) { fadeAnimation(play); if (data.is_paused) { play.innerHTML = `${Spicetify.SVGIcons.play}`; } else { play.innerHTML = `${Spicetify.SVGIcons.pause}`; } } let prevControlData = { shuffle: Spicetify?.Player?.origin?._state?.shuffle, repeat: Spicetify?.Player?.origin?._state?.repeat, }; function updateExtraControls(data) { data = !data ? Spicetify.Player.origin._state : data.data; updateHeart(); if (prevControlData?.shuffle !== data?.shuffle) fadeAnimation(shuffle); if (prevControlData?.repeat !== data?.repeat) fadeAnimation(repeat); prevControlData = { shuffle: data?.shuffle, repeat: data?.repeat, }; repeat.classList.toggle("dot-after", data?.repeat !== 0); repeat.classList.toggle("button-active", data?.repeat !== 0); shuffle.classList.toggle("dot-after", data?.shuffle); shuffle.classList.toggle("button-active", data?.shuffle); if (data?.repeat === 2) { repeat.innerHTML = `${Spicetify.SVGIcons["repeat-once"]}`; } else { repeat.innerHTML = `${Spicetify.SVGIcons["repeat"]}`; } if (data.restrictions) { shuffle.classList.toggle("unavailable", !data?.restrictions?.canToggleShuffle); repeat.classList.toggle("unavailable", !data?.restrictions?.canToggleRepeatTrack && !data?.restrictions?.canToggleRepeatContext); } } let prevHeartData = Spicetify?.Player?.origin?._state?.item?.metadata["collection.in_collection"]; function updateHeart() { const meta = Spicetify?.Player?.origin?._state?.item; heart.classList.toggle("unavailable", meta?.metadata["collection.can_add"] !== "true"); if (prevHeartData !== meta?.metadata["collection.in_collection"]) fadeAnimation(heart); prevHeartData = meta?.metadata["collection.in_collection"]; if (meta?.metadata["collection.in_collection"] === "true" || Spicetify.Player.getHeart()) { heart.innerHTML = `${Spicetify.SVGIcons["heart-active"]}`; heart.classList.add("button-active"); } else { heart.innerHTML = `${Spicetify.SVGIcons["heart"]}`; heart.classList.remove("button-active"); } } let curTimer, ctxTimer, volTimer; function hideCursor() { if (curTimer) { clearTimeout(curTimer); curTimer = 0; } container.style.cursor = "default"; curTimer = setTimeout(() => (container.style.cursor = "none"), 2000); } function hideContext() { if (ctxTimer) { clearTimeout(ctxTimer); ctxTimer = 0; } ctx_container.style.opacity = 1; ctxTimer = setTimeout(() => (ctx_container.style.opacity = 0), 3000); } function hideVolume() { if (volTimer) { clearTimeout(volTimer); volTimer = 0; } volumeContainer.classList.remove("v-hidden"); volTimer = setTimeout(() => volumeContainer.classList.add("v-hidden"), 3000); } let origLoc, progressListener; const heartObserver = new MutationObserver(updateHeart); function activate() { container.style.setProperty("--fs-transition", `${CONFIG[ACTIVE].backAnimationTime}s`); updateInfo(); Spicetify.Player.addEventListener("songchange", updateInfo); container.addEventListener("mousemove", hideCursor); hideCursor(); container.querySelector("#fsd-foreground").oncontextmenu = openConfig; container.querySelector("#fsd-foreground").ondblclick = deactivate; back.oncontextmenu = openConfig; back.ondblclick = deactivate; if (CONFIG[ACTIVE].contextDisplay === "m") { container.addEventListener("mousemove", hideContext); hideContext(); } if (CONFIG[ACTIVE].upnextDisplay) { updateUpNextShow(); Spicetify.Player.origin._events.addListener("queue_update", updateUpNext); Spicetify.Player.origin._events.addListener("update", updateUpNextShow); window.addEventListener("resize", updateUpNext); } if (CONFIG[ACTIVE].volumeDisplay !== "n") { if (Spicetify.Platform?.PlaybackAPI === undefined) Spicetify.Player.origin._events.addListener("volume", updateVolume); else Spicetify.Platform.PlaybackAPI._events.addListener("volume", updateVolume); updateVolume(); if (CONFIG[ACTIVE].volumeDisplay === "m") { container.addEventListener("mousemove", hideVolume); hideVolume(); } } if (CONFIG[ACTIVE].enableFade) { cover.classList.add("fsd-background-fade"); if (CONFIG.tvMode) back.classList.add("fsd-background-fade"); } else { cover.classList.remove("fsd-background-fade"); if (CONFIG.tvMode) back.classList.remove("fsd-background-fade"); } if (CONFIG[ACTIVE].icons) { updatePlayingIcon({ data: { is_paused: !Spicetify.Player.isPlaying() } }); Spicetify.Player.addEventListener("onplaypause", updatePlayingIcon); } if (CONFIG[ACTIVE].progressBarDisplay) { updateProgress(); progressListener = setInterval(updateProgress, 500); } if (CONFIG[ACTIVE].playerControls) { updatePlayerControls({ data: { is_paused: !Spicetify.Player.isPlaying() } }); Spicetify.Player.addEventListener("onplaypause", updatePlayerControls); } if (CONFIG[ACTIVE].extraControls) { updateExtraControls(); addObserver(heartObserver, ".control-button-heart", { attributes: true, attributeFilter: ["aria-checked"] }); Spicetify.Player.origin._events.addListener("update", updateExtraControls); } document.body.classList.add(...classes); if (CONFIG[ACTIVE].enableFullscreen) fullScreenOn(); else fullScreenOff(); document.querySelector(".Root__top-container")?.append(style, container); if (CONFIG[ACTIVE].lyricsDisplay) { window.addEventListener("lyrics-plus-update", handleLyricsUpdate); origLoc = Spicetify.Platform.History.location.pathname; if (origLoc !== "/lyrics-plus") { Spicetify.Platform.History.push("/lyrics-plus"); } window.dispatchEvent(new Event("fad-request")); } Spicetify.Keyboard.registerShortcut( { key: Spicetify.Keyboard.KEYS["F11"], ctrl: false, shift: false, alt: false, }, fsToggle ); Spicetify.Keyboard.registerShortcut( { key: Spicetify.Keyboard.KEYS["ESCAPE"], ctrl: false, shift: false, alt: false, }, deactivate ); } function deactivate() { Spicetify.Player.removeEventListener("songchange", updateInfo); container.removeEventListener("mousemove", hideCursor); if (CONFIG[ACTIVE].contextDisplay === "m") { container.removeEventListener("mousemove", hideContext); } if (CONFIG[ACTIVE].upnextDisplay) { upNextShown = false; Spicetify.Player.origin._events.removeListener("queue_update", updateUpNext); Spicetify.Player.origin._events.removeListener("update", updateUpNextShow); window.removeEventListener("resize", updateUpNext); } if (CONFIG[ACTIVE].volumeDisplay !== "n") { if (Spicetify.Platform?.PlaybackAPI === undefined) Spicetify.Player.origin._events.removeListener("volume", updateVolume); else Spicetify.Platform.PlaybackAPI._events.removeListener("volume", updateVolume); if (CONFIG[ACTIVE].volumeDisplay === "m") container.removeEventListener("mousemove", hideVolume); } if (CONFIG[ACTIVE].progressBarDisplay) { clearInterval(progressListener); } if (CONFIG[ACTIVE].icons) { Spicetify.Player.removeEventListener("onplaypause", updatePlayingIcon); } if (CONFIG[ACTIVE].playerControls) { Spicetify.Player.removeEventListener("onplaypause", updatePlayerControls); } if (CONFIG[ACTIVE].extraControls) { heartObserver.disconnect(); Spicetify.Player.origin._events.removeListener("update", updateExtraControls); } document.body.classList.remove(...classes); upNextShown = false; if (CONFIG[ACTIVE].enableFullscreen) { fullScreenOff(); } let popup = document.querySelector("body > generic-modal"); if (popup) popup.remove(); style.remove(); container.remove(); if (CONFIG[ACTIVE].lyricsDisplay) { window.removeEventListener("lyrics-plus-update", handleLyricsUpdate); if (origLoc !== "/lyrics-plus") { Spicetify.Platform.History.push(origLoc); Spicetify.Platform.History.entries.splice(Spicetify.Platform.History.entries.length - 3, 2); Spicetify.Platform.History.index = Spicetify.Platform.History.index > 0 ? Spicetify.Platform.History.index - 2 : -1; Spicetify.Platform.History.length = Spicetify.Platform.History.length > 1 ? Spicetify.Platform.History.length - 2 : 0; } window.dispatchEvent(new Event("fad-request")); } Spicetify.Keyboard._deregisterShortcut({ key: Spicetify.Keyboard.KEYS["F11"], ctrl: false, shift: false, alt: false, }); Spicetify.Keyboard._deregisterShortcut({ key: Spicetify.Keyboard.KEYS["ESCAPE"], ctrl: false, shift: false, alt: false, }); } function fsToggle() { if (CONFIG[ACTIVE].enableFullscreen) { CONFIG[ACTIVE]["enableFullscreen"] = false; saveConfig(); render(); activate(); } else { CONFIG[ACTIVE]["enableFullscreen"] = true; saveConfig(); render(); activate(); } } function getAvailableLanguages(translations) { let languages = {}; for (const lang in translations) { languages[lang] = translations[lang].langName; } return languages; } /** * Merges keys from one object into another recursively. */ function mergeDefaultsInConfig(defaultObject, objectToMergeIn) { Object.keys(defaultObject).forEach((key) => { if (objectToMergeIn[key] === undefined) { objectToMergeIn[key] = defaultObject[key]; } else { if (typeof defaultObject[key] === "object") { mergeDefaultsInConfig(defaultObject[key], objectToMergeIn[key]); } } }); } function getConfig() { try { const parsed = JSON.parse(localStorage.getItem("full-screen-config") ?? "{}"); if (!!parsed && typeof parsed === "object") { // mergeDefaultsInConfig(DEFAULTS, parsed); Object.keys(DEFAULTS).forEach((key) => { if (parsed[key] === undefined) { parsed[key] = DEFAULTS[key]; } else { if (typeof DEFAULTS[key] === "object") { Object.keys(DEFAULTS[key]) .filter((subkey) => parsed[key][subkey] === undefined) .forEach((subkey) => { parsed[key][subkey] = DEFAULTS[key][subkey]; }); } } }); localStorage.setItem("full-screen-config", JSON.stringify(parsed)); return parsed; } throw ""; } catch { localStorage.setItem("full-screen-config", JSON.stringify(DEFAULTS)); return DEFAULTS; } } function saveConfig() { localStorage.setItem("full-screen-config", JSON.stringify(CONFIG)); } function saveOption(key, value) { CONFIG[ACTIVE][key] = value; saveConfig(); render(); if (document.body.classList.contains("fsd-activated")) activate(); } // Saves an option independent from TV or Fullscreen mode function saveGlobalOption(key, value) { CONFIG[key] = value; LOCALE = CONFIG.locale; // Update locale (avoids reloading client to apply setting) saveConfig(); render(); if (document.body.classList.contains("fsd-activated")) activate(); } function createAdjust(name, key, unit = "", defaultValue, step, min, max, onChange = (val) => {}) { let value = key in CONFIG[ACTIVE] ? CONFIG[ACTIVE][key] : defaultValue; function adjustValue(dir) { let temp = value + dir * step; if (temp < min) { temp = min; } else if (temp > max) { temp = max; } value = Number(Number(temp).toFixed(step >= 1 ? 0 : 1)); container.querySelector(".adjust-value").innerText = `${value}${unit}`; plus && plus.classList.toggle("disabled", value === max); minus && minus.classList.toggle("disabled", value === min); onChange(value); } const container = document.createElement("div"); container.innerHTML = `

${value}${unit}

`; const minus = container.querySelector(".minus"); const plus = container.querySelector(".plus"); minus && minus.classList.toggle("disabled", value === min); plus && plus.classList.toggle("disabled", value === max); minus && (minus.onclick = () => adjustValue(-1)); plus && (plus.onclick = () => adjustValue(1)); return container; } function createOptions(name, options, defaultValue, callback) { const container = document.createElement("div"); container.innerHTML = `
`; const select = container.querySelector("select"); select && (select.value = defaultValue); select && (select.onchange = (e) => { callback(e?.target?.value); }); return container; } function createToggle(name, key, callback = (a,b) => {}) { const container = document.createElement("div"); container.innerHTML = `
`; const toggle = container.querySelector("input"); toggle && (toggle.checked = CONFIG[ACTIVE][key]); toggle && (toggle.onchange = (evt) => { CONFIG[ACTIVE][key] = evt?.target?.checked; saveConfig(); render(); callback(container, evt?.target?.checked); if (document.body.classList.contains("fsd-activated")) { activate(); } }); return container; } function headerText(text, subtext = "") { const container = document.createElement("div"); container.classList.add("subhead"); const listHeader = document.createElement("h2"); listHeader.innerText = text; container.append(listHeader); if (subtext) { const listSub = document.createElement("i"); listSub.innerText = "(" + subtext + ")"; container.append(listSub); } return container; } let configContainer; function openConfig(evt) { evt?.preventDefault(); configContainer = document.createElement("div"); configContainer.id = "full-screen-config-container"; const style = document.createElement("style"); style.innerHTML = ` .GenericModal ::-webkit-scrollbar{ width: 7px; } .GenericModal ::-webkit-scrollbar-thumb{ border-radius: 2rem; } .main-trackCreditsModal-container{ height: 75vh; max-height: 600px; width: clamp(500px,50vw,600px); } .setting-row::after { content: ""; display: table; clear: both; } .setting-row-but{ display: flex; align-items: center; justify-content: center; } .subhead{ text-align: center; padding: 5px 0; } .setting-row-but button{ margin: 5px 10px; } .setting-row .col { display: flex; padding: 10px 0; align-items: center; } .setting-row .col.description { float: left; padding-right: 15px; } .setting-row .col.action { float: right; text-align: right; } .switch { position: relative; display: inline-block; width: 44px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(150,150,150,.5); transition: all .3s ease-in-out; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 24px; width: 24px; left: -2px; bottom: -2px; background-color: #EEE; transition: all .3s ease-in-out; border-radius: 50%; } input:checked + .slider { background-color: rgba(var(--spice-rgb-button-disabled),.6); } input:focus + .slider { box-shadow: 0 0 1px rgba(var(--spice-rgb-button-disabled),.6); } input:checked + .slider:before { transform: translateX(24px); background-color: var(--spice-button); filter: brightness(1.1) saturate(1.2); } #full-screen-config-container select { color: var(--spice-text); background: var(--spice-card); border: none; height: 32px; } #full-screen-config-container option { color: var(--spice-text); background: var(--spice-card); } button.switch { align-items: center; border: 0px; border-radius: 50%; background-color: rgba(var(--spice-rgb-shadow), 0.7); color: var(--spice-text); cursor: pointer; margin-inline-start: 12px; padding: 8px; width: 32px; height: 32px; } button.switch.disabled, button.switch[disabled] { color: rgba(var(--spice-rgb-text), 0.3); pointer-events: none; } button.switch.small { width: 22px; height: 22px; padding: 3px; } .main-buttons-button{ border: 2px solid transparent; border-radius: 500px; color: var(--spice-text); cursor: pointer; display: inline-block; font-size: 12px; font-weight: 700; letter-spacing: 1.76px; line-height: 18px; padding: 8px 34px; text-align: center; text-transform: uppercase; -webkit-transition: all 33ms cubic-bezier(.3,0,0,1); transition: all 33ms cubic-bezier(.3,0,0,1); white-space: nowrap; will-change: transform; } .main-button-primary{ background-color: var(--spice-button); } .main-button-secondary{ background-color: transparent; color: var(--spice-button); border: 2px solid var(--spice-button); } #full-screen-config-container .adjust-value { margin-inline: 12px; width: 22px; text-align: center; } `; configContainer.append( style, (() => { const container = document.createElement("div"); container.innerHTML = `
`; container.querySelector("#mode-exit").onclick = deactivate; container.querySelector("#mode-switch").onclick = () => { CONFIG.tvMode ? openwithDef() : openwithTV(); document.querySelector("body > generic-modal")?.remove(); }; return document.body.classList.contains("fsd-activated") ? container : ""; })(), headerText(translations[LOCALE].settings.pluginSettings), createOptions(translations[LOCALE].settings.language, getAvailableLanguages(translations), CONFIG["locale"], (value) => saveGlobalOption("locale", value) ), headerText(translations[LOCALE].settings.lyricsHeader), createToggle(translations[LOCALE].settings.lyrics, "lyricsDisplay", (row, status) => { container.classList.remove("lyrics-unavailable"); let nextEle = row.nextElementSibling; while (!nextEle.classList.contains("subhead")) { nextEle.classList.toggle("hidden", !status); nextEle = nextEle.nextElementSibling; } }), createOptions( translations[LOCALE].settings.lyricsAlignment.setting, { left: translations[LOCALE].settings.lyricsAlignment.left, center: translations[LOCALE].settings.lyricsAlignment.center, right: translations[LOCALE].settings.lyricsAlignment.right, }, CONFIG[ACTIVE].lyricsAlignment, (value) => saveOption("lyricsAlignment", value) ), createAdjust(translations[LOCALE].settings.lyricsAnimationTempo, "animationTempo", "s", 0.3, 0.1, 0, 1, (state) => { CONFIG[ACTIVE]["animationTempo"] = state; saveConfig(); render(); if (document.body.classList.contains("fsd-activated")) activate(); }), headerText(translations[LOCALE].settings.generalHeader), createToggle(translations[LOCALE].settings.progressBar, "progressBarDisplay"), createToggle(translations[LOCALE].settings.playerControls, "playerControls"), createToggle(translations[LOCALE].settings.trimTitle, "trimTitle"), createToggle(translations[LOCALE].settings.trimTitleUpNext, "trimTitleUpNext"), createOptions( translations[LOCALE].settings.showAlbum.setting, { n: translations[LOCALE].settings.showAlbum.never, a: translations[LOCALE].settings.showAlbum.always, d: translations[LOCALE].settings.showAlbum.date, }, CONFIG[ACTIVE].showAlbum, (value) => saveOption("showAlbum", value) ), createToggle(translations[LOCALE].settings.showAllArtists, "showAllArtists"), createToggle(translations[LOCALE].settings.icons, "icons"), createToggle(translations[LOCALE].settings.songChangeAnimation, "enableFade"), document.fullscreenEnabled ? createToggle(translations[LOCALE].settings.fullscreen, "enableFullscreen") : "", headerText(translations[LOCALE].settings.extraHeader), ACTIVE !== "tv" ? createOptions( translations[LOCALE].settings.backgroundChoice.setting, { c: translations[LOCALE].settings.backgroundChoice.color, a: translations[LOCALE].settings.backgroundChoice.artwork, }, CONFIG.def.backgroundChoice, (value) => saveOption("backgroundChoice", value) ) : "", createToggle(translations[LOCALE].settings.extraControls, "extraControls"), createToggle(translations[LOCALE].settings.upnextDisplay, "upnextDisplay"), createOptions( translations[LOCALE].settings.contextDisplay.setting, { n: translations[LOCALE].settings.contextDisplay.never, m: translations[LOCALE].settings.contextDisplay.mouse, a: translations[LOCALE].settings.contextDisplay.always, }, CONFIG[ACTIVE].contextDisplay, (value) => saveOption("contextDisplay", value) ), createOptions( translations[LOCALE].settings.volumeDisplay.setting, { a: translations[LOCALE].settings.volumeDisplay.always, n: translations[LOCALE].settings.volumeDisplay.never, m: translations[LOCALE].settings.volumeDisplay.mouse, o: translations[LOCALE].settings.volumeDisplay.volume, }, CONFIG[ACTIVE].volumeDisplay, (value) => saveOption("volumeDisplay", value) ), headerText(translations[LOCALE].settings.appearanceHeader, translations[LOCALE].settings.appearanceSubHeader), ACTIVE !== "tv" ? createOptions( translations[LOCALE].settings.backgroundColor.setting, { VIBRANT: translations[LOCALE].settings.backgroundColor.vibrant, PROMINENT: translations[LOCALE].settings.backgroundColor.prominent, DESATURATED: translations[LOCALE].settings.backgroundColor.desaturated, LIGHT_VIBRANT: translations[LOCALE].settings.backgroundColor.lightVibrant, DARK_VIBRANT: translations[LOCALE].settings.backgroundColor.darkVibrant, VIBRANT_NON_ALARMING: translations[LOCALE].settings.backgroundColor.vibrantNonAlarming, }, CONFIG.def.coloredBackChoice, (value) => saveOption("coloredBackChoice", value) ) : "", createToggle(translations[LOCALE].settings.themedButtons, "themedButtons"), createToggle(translations[LOCALE].settings.themedIcons, "themedIcons"), createOptions( translations[LOCALE].settings.invertColors.setting, { n: translations[LOCALE].settings.invertColors.never, a: translations[LOCALE].settings.invertColors.always, d: translations[LOCALE].settings.invertColors.auto, }, CONFIG[ACTIVE].invertColors, (value) => saveOption("invertColors", value) ), createAdjust(translations[LOCALE].settings.backAnimationTime, "backAnimationTime", "s", 0.8, 0.1, 0, 5, (state) => { CONFIG[ACTIVE]["backAnimationTime"] = state; saveConfig(); render(); if (document.body.classList.contains("fsd-activated")) activate(); }), createOptions( translations[LOCALE].settings.upnextScroll.setting, { mq: translations[LOCALE].settings.upnextScroll.mq, sp: translations[LOCALE].settings.upnextScroll.sp, }, CONFIG[ACTIVE].upNextAnim, (value) => saveOption("upNextAnim", value) ), createAdjust(translations[LOCALE].settings.upnextTime, "upnextTimeToShow", "s", 30, 1, 5, 60, (state) => { CONFIG[ACTIVE]["upnextTimeToShow"] = state; saveConfig(); updateUpNextShow(); // render() // if (document.body.classList.contains('fsd-activated')) // activate() }), createAdjust(translations[LOCALE].settings.backgroundBlur, "blurSize", "px", 20, 4, 0, 100, (state) => { CONFIG[ACTIVE]["blurSize"] = state; saveConfig(); render(); if (document.body.classList.contains("fsd-activated")) activate(); }), createOptions( translations[LOCALE].settings.backgroundBrightness, { 0: "0%", 0.1: "10%", 0.2: "20%", 0.3: "30%", 0.4: "40%", 0.5: "50%", 0.6: "60%", 0.7: "70%", 0.8: "80%", 0.9: "90%", 1: "100%", }, CONFIG[ACTIVE].backgroundBrightness, (value) => saveOption("backgroundBrightness", Number(value)) ), (() => { const container = document.createElement("div"); container.innerHTML = `
`; container.querySelector("#reset-switch").onclick = () => { CONFIG[ACTIVE] = DEFAULTS[ACTIVE]; saveConfig(); render(); if (document.body.classList.contains("fsd-activated")) { activate(); } configContainer = ""; setTimeout(openConfig, 5); }; container.querySelector("#reload-switch").onclick = () => { location.reload(); }; return container; })() ); Spicetify.PopupModal.display({ title: ACTIVE === "tv" ? translations[LOCALE].settings.tvModeConfig : translations[LOCALE].settings.fullscreenConfig, content: configContainer, }); } // container.ondblclick = deactivatemenu // Add Full Screen Button on bottom bar const defButton = document.createElement("button"); defButton.classList.add("button", "fsd-button", "control-button", "InvalidDropTarget"); defButton.id = "fs-button"; defButton.setAttribute("title", translations[LOCALE].fullscreenBtnDesc); defButton.innerHTML = ``; defButton.onclick = openwithDef; defButton.oncontextmenu = (evt) => { evt.preventDefault(); ACTIVE = "def"; openConfig(); }; // if (CONFIG.fsHideOriginal) { // if (extraBar.lastChild.classList.contains("control-button") || extraBar.lastChild.title == "Full screen") extraBar.lastChild.remove(); // } extraBar?.append(defButton); // Add TV Mode Button on top bar const tvButton = document.createElement("button"); tvButton.classList.add("button", "tm-button", "main-topBar-button", "InvalidDropTarget"); tvButton.innerHTML = ``; tvButton.id = "TV-button"; tvButton.setAttribute("title", translations[LOCALE].tvBtnDesc); tvButton.onclick = openwithTV; topBar?.append(tvButton); tvButton.oncontextmenu = (evt) => { evt.preventDefault(); ACTIVE = "tv"; openConfig(); }; render(); } fullScreen();