From 7771814cf2f525d534f889375a22d30eae84cf1d Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Wed, 2 Jun 2021 22:51:19 +0200 Subject: [PATCH] NEW: light/dark toggle button, dynamic colors, background cover --- README.md | 7 +- Vibrant.min.js | 23 ++++ dribbblish-dynamic.js | 264 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 Vibrant.min.js create mode 100644 dribbblish-dynamic.js diff --git a/README.md b/README.md index 9510158..7b591fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dribbblish +# Dribbblish Dynamic ### base ![base](base.png) @@ -49,6 +49,8 @@ cd "$(dirname "$(spicetify -c)")/Themes/Dribbblish" mkdir -p ../../Extensions cp dribbblish.js ../../Extensions/. spicetify config extensions dribbblish.js +spicetify config extensions dribbblish-dynamic.js +spicetify config extensions Vibrant.min.js spicetify config current_theme Dribbblish color_scheme base spicetify config inject_css 1 replace_colors 1 overwrite_assets 1 spicetify apply @@ -60,6 +62,8 @@ In **Powershell**: cd "$(spicetify -c | Split-Path)\Themes\Dribbblish" Copy-Item dribbblish.js ..\..\Extensions spicetify config extensions dribbblish.js +spicetify config extensions dribbblish-dynamic.js +spicetify config extensions Vibrant.min.js spicetify config current_theme Dribbblish color_scheme base spicetify config inject_css 1 replace_colors 1 overwrite_assets 1 spicetify apply @@ -88,5 +92,6 @@ Remove the dribbblish script with the following commands ``` spicetify config extensions dribbblish.js- +spicetify config extensions dribbblish-dynamic.js- spicetify apply ``` diff --git a/Vibrant.min.js b/Vibrant.min.js new file mode 100644 index 0000000..d858a58 --- /dev/null +++ b/Vibrant.min.js @@ -0,0 +1,23 @@ +(function e$$0(x,z,l){function h(p,b){if(!z[p]){if(!x[p]){var a="function"==typeof require&&require;if(!b&&a)return a(p,!0);if(g)return g(p,!0);a=Error("Cannot find module '"+p+"'");throw a.code="MODULE_NOT_FOUND",a;}a=z[p]={exports:{}};x[p][0].call(a.exports,function(a){var b=x[p][1][a];return h(b?b:a)},a,a.exports,e$$0,x,z,l)}return z[p].exports}for(var g="function"==typeof require&&require,w=0;wg?1:0},sum:function(h,g){var l={};return h.reduce(g?function(h,b,a){l.index=a;return h+g.call(l,b)}:function(h,b){return h+b},0)},max:function(h,g){return Math.max.apply(null,g?l.map(h,g):h)}};A=function(){function h(f,c,a){return(f<<2*d)+(c<>e;m=f[1]>>e;r=f[2]>>e;a=h(b,m,r);c[a]=(c[a]||0)+1});return c} +function a(f,c){var a=1E6,b=0,m=1E6,d=0,q=1E6,n=0,h,k,l;f.forEach(function(c){h=c[0]>>e;k=c[1]>>e;l=c[2]>>e;hb&&(b=h);kd&&(d=k);ln&&(n=l)});return new w(a,b,m,d,q,n,c)}function n(a,c){function b(a){var f=a+"1";a+="2";var v,d,m,e;d=0;for(k=c[f];k<=c[a];k++)if(y[k]>n/2){m=c.copy();e=c.copy();v=k-c[f];d=c[a]-k;for(v=v<=d?Math.min(c[a]-1,~~(k+d/2)):Math.max(c[f],~~(k-1-v/2));!y[v];)v++;for(d=s[v];!d&&y[v-1];)d=s[--v];m[a]=v;e[f]=m[a]+1;return[m,e]}}if(c.count()){var d=c.r2- +c.r1+1,m=c.g2-c.g1+1,e=l.max([d,m,c.b2-c.b1+1]);if(1==c.count())return[c.copy()];var n=0,y=[],s=[],k,g,t,u,p;if(e==d)for(k=c.r1;k<=c.r2;k++){u=0;for(g=c.g1;g<=c.g2;g++)for(t=c.b1;t<=c.b2;t++)p=h(k,g,t),u+=a[p]||0;n+=u;y[k]=n}else if(e==m)for(k=c.g1;k<=c.g2;k++){u=0;for(g=c.r1;g<=c.r2;g++)for(t=c.b1;t<=c.b2;t++)p=h(g,k,t),u+=a[p]||0;n+=u;y[k]=n}else for(k=c.b1;k<=c.b2;k++){u=0;for(g=c.r1;g<=c.r2;g++)for(t=c.g1;t<=c.g2;t++)p=h(g,t,k),u+=a[p]||0;n+=u;y[k]=n}y.forEach(function(a,c){s[c]=n-a});return e== +d?b("r"):e==m?b("g"):b("b")}}var d=5,e=8-d;w.prototype={volume:function(a){if(!this._volume||a)this._volume=(this.r2-this.r1+1)*(this.g2-this.g1+1)*(this.b2-this.b1+1);return this._volume},count:function(a){var c=this.histo;if(!this._count_set||a){a=0;var b,d,n;for(b=this.r1;b<=this.r2;b++)for(d=this.g1;d<=this.g2;d++)for(n=this.b1;n<=this.b2;n++)index=h(b,d,n),a+=c[index]||0;this._count=a;this._count_set=!0}return this._count},copy:function(){return new w(this.r1,this.r2,this.g1,this.g2,this.b1, +this.b2,this.histo)},avg:function(a){var c=this.histo;if(!this._avg||a){a=0;var b=1<<8-d,n=0,e=0,g=0,q,l,s,k;for(l=this.r1;l<=this.r2;l++)for(s=this.g1;s<=this.g2;s++)for(k=this.b1;k<=this.b2;k++)q=h(l,s,k),q=c[q]||0,a+=q,n+=q*(l+0.5)*b,e+=q*(s+0.5)*b,g+=q*(k+0.5)*b;this._avg=a?[~~(n/a),~~(e/a),~~(g/a)]:[~~(b*(this.r1+this.r2+1)/2),~~(b*(this.g1+this.g2+1)/2),~~(b*(this.b1+this.b2+1)/2)]}return this._avg},contains:function(a){var c=a[0]>>e;gval=a[1]>>e;bval=a[2]>>e;return c>=this.r1&&c<=this.r2&& +gval>=this.g1&&gval<=this.g2&&bval>=this.b1&&bval<=this.b2}};p.prototype={push:function(a){this.vboxes.push({vbox:a,color:a.avg()})},palette:function(){return this.vboxes.map(function(a){return a.color})},size:function(){return this.vboxes.size()},map:function(a){for(var c=this.vboxes,b=0;bb[0]&&5>b[1]&&5>b[2]&&(a[0].color=[0,0,0]);var b=a.length-1,n=a[b].color;251d;)if(f=a.pop(),f.count()){var m=n(h,f);f=m[0];m=m[1];if(!f)break; +a.push(f);m&&(a.push(m),c++);if(c>=b)break;if(1E3c||256this.yiq?"#fff":"#000"};b.prototype.getBodyTextColor=function(){this._ensureTextColors();return 150>this.yiq?"#fff":"#000"};b.prototype._ensureTextColors=function(){if(!this.yiq)return this.yiq=(299*this.rgb[0]+587*this.rgb[1]+114*this.rgb[2])/1E3};return b}();window.Vibrant=g=function(){function b(a,b,d){this.swatches=w(this.swatches,this);var e,f, +c,g,p,m,r,q;"undefined"===typeof b&&(b=64);"undefined"===typeof d&&(d=5);p=new l(a);r=p.getImageData().data;m=p.getPixelCount();a=[];for(g=0;g=f&&s<=c&&m>=b&&m<=d&&!this.isAlreadySelected(k)&&(m=this.createComparisonValue(s,e,m,a, +k.getPopulation(),this.HighestPopulation),void 0===l||m>q))l=k,q=m;return l};b.prototype.createComparisonValue=function(a,b,d,e,f,c){return this.weightedMean(this.invertDiff(a,b),this.WEIGHT_SATURATION,this.invertDiff(d,e),this.WEIGHT_LUMA,f/c,this.WEIGHT_POPULATION)};b.prototype.invertDiff=function(a,b){return 1-Math.abs(a-b)};b.prototype.weightedMean=function(){var a,b,d,e,f,c;f=1<=arguments.length?p.call(arguments,0):[];for(a=d=b=0;ac&&(c+=1);1c?b:c<2/3?a+(b-a)*(2/3-c)*6:a};0===b?c=f=e=d:(b=0.5>d?d*(1+b):d+b-d*b,d=2*d-b,c=e(d,b,a+1/3),f=e(d,b,a),e=e(d,b,a-1/3));return[255*c,255*f,255*e]};return b}();window.CanvasImage=l=function(){function b(a){this.canvas= +document.createElement("canvas");this.context=this.canvas.getContext("2d");document.body.appendChild(this.canvas);this.width=this.canvas.width=a.width;this.height=this.canvas.height=a.height;this.context.drawImage(a,0,0,this.width,this.height)}b.prototype.clear=function(){return this.context.clearRect(0,0,this.width,this.height)};b.prototype.update=function(a){return this.context.putImageData(a,0,0)};b.prototype.getPixelCount=function(){return this.width*this.height};b.prototype.getImageData=function(){return this.context.getImageData(0, +0,this.width,this.height)};b.prototype.removeCanvas=function(){return this.canvas.parentNode.removeChild(this.canvas)};return b}()}).call(this)},{quantize:1}]},{},[2]); diff --git a/dribbblish-dynamic.js b/dribbblish-dynamic.js new file mode 100644 index 0000000..72f52e6 --- /dev/null +++ b/dribbblish-dynamic.js @@ -0,0 +1,264 @@ +let current = '2.3' + +function waitForElement(els, func, timeout = 100) { + const queries = els.map(el => document.querySelector(el)); + if (queries.every(a => a)) { + func(queries); + } else if (timeout > 0) { + setTimeout(waitForElement, 300, els, func, --timeout); + } +} + +function getAlbumInfo(uri) { + return Spicetify.CosmosAsync.get(`hm://album/v1/album-app/album/${uri}/desktop`); +} + +function isLight(hex) { + var [r,g,b] = hexToRgb(hex).map(Number) + const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000 + return brightness > 128 +} + +function hexToRgb(hex) { + var bigint = parseInt(hex.replace("#",""), 16) + var r = (bigint >> 16) & 255 + var g = (bigint >> 8) & 255 + var b = bigint & 255 + return [r, g, b] +} + +function rgbToHex([r, g, b]) { + const rgb = (r << 16) | (g << 8) | (b << 0); + return '#' + (0x1000000 + rgb).toString(16).slice(1); +} + +const LightenDarkenColor = (h, p) => '#' + [1, 3, 5].map(s => parseInt(h.substr(s, 2), 16)).map(c => parseInt((c * (100 + p)) / 100)).map(c => (c < 255 ? c : 255)).map(c => c.toString(16).padStart(2, '0')).join(''); + +function rgbToHsl([r, g, b]) { + r /= 255, g /= 255, b /= 255; + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2; + if (max == min) { + h = s = 0; // achromatic + } else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return [h, s, l]; +} + +function hslToRgb([h, s, l]) { + var r, g, b; + if (s == 0) { + r = g = b = l; // achromatic + } else { + function hue2rgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + return [r * 255, g * 255, b * 255]; +} + +function setLightness(hex, lightness) { + hsl = rgbToHsl(hexToRgb(hex)) + hsl[2] = lightness + return rgbToHex(hslToRgb(hsl)) +} + +let nearArtistSpan = null +let mainColor = getComputedStyle(document.documentElement).getPropertyValue('--spice-text') +let mainColorBg = getComputedStyle(document.documentElement).getPropertyValue('--spice-main') + +waitForElement([".main-trackInfo-container"], (queries) => { + nearArtistSpan = document.createElement("div") + nearArtistSpan.classList.add("main-trackInfo-artists", "ellipsis-one-line", "main-type-finale") + queries[0].append(nearArtistSpan) +}); + +function setRootColor(name, colHex) { + let root = document.documentElement + if (root===null) return + root.style.setProperty('--spice-' + name, colHex) + root.style.setProperty('--spice-rgb-' + name, hexToRgb(colHex).join(',')) +} + +function toggleDark(setDark) { + if (setDark===undefined) setDark = isLight(mainColorBg) + + document.documentElement.style.setProperty('--is_light', setDark ? 0 : 1) + mainColorBg = setDark ? "#0A0A0A" : "#FAFAFA" + + setRootColor('main', mainColorBg) + setRootColor('player', mainColorBg) + setRootColor('card', setDark ? "#040404" : "#ECECEC") + setRootColor('subtext', setDark ? "#EAEAEA" : "#3D3D3D") + setRootColor('notification', setDark ? "#303030" : "#DDDDDD") + + updateColors(mainColor) +} + +/* Init with current system light/dark mode */ +let systemDark = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--system_is_dark'))==1 +toggleDark(systemDark) + +waitForElement([".main-topBar-indicators"], (queries) => { + // Add activator on top bar + const div = document.createElement("div") + div.id = 'main-topBar-moon-div' + div.classList.add("main-topBarStatusIndicator-TopBarStatusIndicatorContainer") + queries[0].append(div) + + const button = document.createElement("button") + button.id = 'main-topBar-moon-button' + button.classList.add("main-topBarStatusIndicator-TopBarStatusIndicator", "main-topBarStatusIndicator-hasTooltip") + button.setAttribute("title", "Light/Dark") + button.onclick = () => { toggleDark(); }; + button.innerHTML = ` +` + div.append(button) +}); + +function updateColors(colHex) { + let isLightBg = isLight(mainColorBg) + if( isLightBg ) colHex = LightenDarkenColor(colHex, -15) // vibrant color is always too bright for white bg mode + + let darkColHex = LightenDarkenColor(colHex, isLightBg ? 12 : -20) + let darkerColHex = LightenDarkenColor(colHex, isLightBg ? 30 : -40) + let buttonBgColHex = setLightness(colHex, isLightBg ? 0.90 : 0.14) + setRootColor('text', colHex) + setRootColor('button', darkerColHex) + setRootColor('button-active', darkColHex) + setRootColor('selected-row', darkerColHex) + setRootColor('tab-active', buttonBgColHex) + setRootColor('button-disabled', buttonBgColHex) + setRootColor('sidebar', colHex) +} + +async function songchange() { + // warning popup + if (Spicetify.PlaybackControl.featureVersion < "1.1.57") + Spicetify.showNotification("Your version of Spotify (" + Spicetify.PlaybackControl.featureVersion + ") is un-supported") + + let album_uri = Spicetify.Player.data.track.metadata.album_uri + let bgImage = Spicetify.Player.data.track.metadata.image_url + if (bgImage === undefined) { + bgImage = "/images/tracklist-row-song-fallback.svg" + mainColor = "#509bf5" + updateColors(mainColor) + } + + if (album_uri !== undefined && !album_uri.includes('spotify:show')) { + const albumInfo = await getAlbumInfo(album_uri.replace("spotify:album:", "")) + + let album_date = new Date(albumInfo.year, (albumInfo.month || 1)-1, albumInfo.day|| 0) + let recent_date = new Date() + recent_date.setMonth(recent_date.getMonth() - 6) + album_date = album_date.toLocaleString('default', album_date>recent_date ? { year: 'numeric', month: 'short' } : { year: 'numeric' }) + album_link = ""+Spicetify.Player.data.track.metadata.album_title+"" + + if (nearArtistSpan!==null) + nearArtistSpan.innerHTML = album_link + " — " + album_date + else + setTimeout(songchange, 200) + } else if (Spicetify.Player.data.track.uri.includes('spotify:episode')) { + // podcast + bgImage = bgImage.replace('spotify:image:', 'https://i.scdn.co/image/') + if (nearArtistSpan?.innerText) nearArtistSpan.innerText = Spicetify.Player.data.track.metadata.album_title + } else if (Spicetify.Player.data.track.metadata.is_local=="true") { + // local file + if (nearArtistSpan?.innerText) nearArtistSpan.innerText = Spicetify.Player.data.track.metadata.album_title + } else { + // When clicking a song from the homepage, songChange is fired with half empty metadata + // todo: retry only once? + setTimeout(songchange, 200) + } + + document.documentElement.style.setProperty('--image_url', 'url("' + bgImage + '")') +} + +Spicetify.Player.addEventListener("songchange", songchange) + +function pickCoverColor(img) { + var swatches = new Vibrant(img, 12).swatches() + cols = isLight(mainColorBg) ? ["Vibrant", "DarkVibrant", "LightVibrant", "Muted"] + : ["Vibrant", "LightVibrant", "DarkVibrant", "Muted"] + mainColor = "#509bf5" + for (var col in cols) + if (swatches[cols[col]]) { + mainColor = swatches[cols[col]].getHex() + break + } + updateColors(mainColor) +} + +waitForElement([".cover-art-image"], (queries) => { + queries[0].addEventListener('load', function() { + try { + pickCoverColor(queries[0]) + } catch (error) { + console.log(error); + setTimeout(pickCoverColor, 300, queries[0]) + } + }); +}); + +(function Startup() { + if (!Spicetify.showNotification) { + setTimeout(Startup, 300) + return + } + // Check latest release + fetch('https://api.github.com/repos/JulienMaille/dribbblish-dynamic-theme/releases/latest').then(response => { + return response.json(); + }).then(data => { + if( data.tag_name > current ) { + document.querySelector("#main-topBar-moon-div").classList.add("main-topBarUpdateAvailable") + document.querySelector("#main-topBar-moon-button").append(`NEW v${data.tag_name} available`) + document.querySelector("#main-topBar-moon-button").setAttribute("title", `Changes: ${data.name}`) + } + }).catch(err => { + // Do something for an error here + }); + Spicetify.showNotification("Applied system " + (systemDark ? "dark" : "light") + " theme.") +})() + +/* translucid background cover */ +document.styleSheets[0].addRule('p.special:before','content: "'+str+'";'); + +document.styleSheets[0].addRule('.main-view-container__scroll-node-child::before', +` content: ''; + background-image: var(--image_url); + background-repeat: no-repeat; + background-size: cover; + background-position: center center; + position: fixed; + display: block; + top: 0; + left: 0; + right: 0; + bottom: 0; + filter: blur(15px); + pointer-events: none; + backface-visibility: hidden; + will-change: transform; + transition: background-image var(--transition); + opacity: calc(0.07 + 0.03 * var(--is_light, 0)); + z-index: +3;`) + +document.documentElement.style.setProperty('--warning_message', ' '); \ No newline at end of file