// @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 ? `` : ""}
${
CONFIG[ACTIVE].showAlbum !== "n"
? `
${CONFIG[ACTIVE].icons ? `` : ""}
`
: ""
}
${
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 = ``;
else if (currVol > 30)
volumeIcon.innerHTML = ``;
else if (currVol > 0)
volumeIcon.innerHTML = ``;
else
volumeIcon.innerHTML = ``;
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 = ``;
} else {
play.innerHTML = ``;
}
}
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 = ``;
} else {
repeat.innerHTML = ``;
}
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 = ``;
heart.classList.add("button-active");
} else {
heart.innerHTML = ``;
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();