Merge remote-tracking branch 'origin/webpack'

This commit is contained in:
Send_Nukez 2021-10-28 17:46:13 +02:00 committed by GitHub Action
commit 6161f6808f
27 changed files with 2633 additions and 2498 deletions

34
.github/workflows/empty-changelog.yaml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Empty CHANGELOG.md
on:
release:
types: [published]
jobs:
empty-changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.repository.default_branch }}
- name: Empty CHANGELOG.md
run: |
git config --global user.email "action@github.com"
git config --global user.name "github-actions"
if [ -s CHANGELOG.md ]; then
echo "Comitting emptied CHANGELOG.md"
else
echo "CHANGELOG.md is already empty. skipping comitting"
exit 0
fi
rm CHANGELOG.md
touch CHANGELOG.md
git add CHANGELOG.md
git commit -m "Empty CHANGELOG.md"
git push

73
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,73 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version of Release (format: X.X.X)'
required: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Verify Input
run: |
[[ "${{ github.event.inputs.version }}" =~ ^[0-9]\.[0-9]\.[0-9]$ ]] && echo "Matches" && exit 0 || echo "Use versions like '1.2.3'" && exit 1
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Build Webpack
run: |
npm install
npm run build
echo "${{ github.event.inputs.version }}" > dist/VERSION
env:
DRIBBBLISH_VERSION: ${{ github.event.inputs.version }}
- name: Zip Release
working-directory: dist
run: |
sudo apt-get install zip
zip -r DribbblishDynamic_v${{ github.event.inputs.version }}.zip *
mv DribbblishDynamic_v${{ github.event.inputs.version }}.zip ..
- name: Read CHANGELOG.md
run: |
[ -s CHANGELOG.md ] && CHANGELOG=$(< CHANGELOG.md) || CHANGELOG="*Empty.*"
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
fail_on_unmatched_files : true
files: DribbblishDynamic_v${{ github.event.inputs.version }}.zip
tag_name: ${{ github.event.inputs.version }}
draft: true
name: v${{ github.event.inputs.version }}
body: |
## Changelog
${{ env.CHANGELOG }}
---
### Install / Update
#### Windows (PowerShell)
```powershell
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/master/install.ps1" | Invoke-Expression
```
#### Linux/MacOS (Bash)
```bash
curl -fsSL https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/master/install.sh | sh
```

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -1 +1,2 @@
Vibrant.min.js
Vibrant.min.js
dist/

0
CHANGELOG.md Normal file
View file

View file

@ -1,24 +1,25 @@
# Dribbblish Dynamic
### Preview
![preview](showcase-images/preview.gif)
<img src="showcase-images/preview.gif" alt="img" width="500px">
## Features
### Resizable sidebar
<img src="https://i.imgur.com/1zomkmd.png" alt="img" width="500px">
<img src="showcase-images/resize-sidebar.png" alt="img" width="500px">
### Customizable sidebar
Rearrange icons positions, stick icons to header or hide unnecessary to save space.
Turn on "Sidebar config" mode in Profile menu and hover on icon to show control buttons.
After you finish customizing, turn off Config mode in Profile menu to save.
<img src="https://i.imgur.com/86gqPe8.png" alt="img" width="500px">
<img src="showcase-images/customize-sidebar.png" alt="img" width="500px">
### Playlist Folder image
Right click at folder and choose images for your playlist folder. Every image formats supported by Chrome can be used, but do keep image size small and in compressed format.
<img src="https://i.imgur.com/WGQ7Bev.gif" alt="img" width="500px">
<img src="showcase-images/playlist-folders.gif" alt="img" width="500px">
### Left/Right expanded cover
In profile menu, toggle option "Right expanded cover" to change expaned current track cover image to left or right side, whereever you prefer.
@ -26,20 +27,30 @@ In profile menu, toggle option "Right expanded cover" to change expaned current
## Install / Update
Make sure you are using spicetify >= v2.6.0 and Spotify >= v1.1.67.
Run these commands:
### Windows
In **Powershell**:
### Windows (PowerShell)
```powershell
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/master/install.ps1" | Invoke-Expression
```
### Linux and MacOS:
In **Bash**:
### Linux/MacOS (Bash)
```bash
curl -fsSL https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/master/install.sh | sh
```
### Manual Install
1. Download the latest [DribbblishDynamic_vX.X.X.zip](https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest)
2. Extract the files to your [Spicetify/Themes folder](https://github.com/khanhas/spicetify-cli/wiki/Customization#themes)
3. Copy `dribbblish-dynamic.js` to your [Spicetify/Extensions folder](https://github.com/khanhas/spicetify-cli/wiki/Extensions#installing)
4. Run:
```
spicetify config extensions dribbblish-dynamic.js
spicetify config current_theme DribbblishDynamic
spicetify config color_scheme base
spicetify config inject_css 1 replace_colors 1 overwrite_assets 1
spicetify apply
```
## IMPORTANT!
From Spotify > v1.1.62, in sidebar, they use an adaptive render mechanic to actively show and hide items on scroll. It helps reducing number of items to render, hence there is significant performance boost if you have a large playlists collection. But the drawbacks is that item height is hard-coded, it messes up user interaction when we explicity change, in CSS, playlist item height bigger than original value. So you need to add these 2 lines in Patch section in config file:
```ini
[Patch]
@ -50,30 +61,23 @@ xpui.js_repl_8008 = ,${1}56,
## Hide Window Controls
Windows user, please edit your Spotify shortcut and add flag `--transparent-window-controls` after the Spotify.exe:
To edit an taskbar shortcut, right click it, then right click Spotify in the list again.
![instruction1](showcase-images/windows-shortcut-instruction.png)
<img src="showcase-images/windows-shortcut-instruction.png" alt="img" width="500px">
In addition to `--transparent-window-controls` you can set `Windows Top Bars` to `Solid` or `Transparent` to look like this:
![top-bars](showcase-images/top-bars.png)
Alternatively, you can use [`SpotifyNoControl.exe`](https://github.com/JulienMaille/SpotifyNoControl/files/6793911/SpotifyNoControl.zip), included in this theme package, to completely remove all windows controls and title menu (three dots at top left corner). Title menu still can be accessed via the Alt key. Closing and minimizing can be done via the right click menu at top window region.
`SpotifyNoControl.exe` could be used as Spotify launcher, it opens Spotify and hides controls right after. You can drag and drop it to your taskbar but make sure you unpin the original Spotify icon first. Alternatively you can make a shortcut for it and add to desktop or start menu.
<img src="showcase-images/top-bars.png" alt="img" width="500px">
![nocontrol](https://i.imgur.com/qdZyv1t.png)
## Auto-uninstall
### Windows
## Uninstall
### Windows (PowerShell)
```powershell
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/master/uninstall.ps1" | Invoke-Expression
```
## Manual uninstall
Remove the dribbblish script with the following commands
```
spicetify config extensions dribbblish.js-
spicetify config extensions dribbblish-dynamic.js-
```
And remove Patch lines you added in config file earlier. Finally, run:
```
spicetify apply
```
### Manual Uninstall
1. Remove Patch lines you added in config file earlier.
2. Run:
```
spicetify config extensions dribbblish-dynamic.js-
spicetify apply
```

23
Vibrant.min.js vendored
View file

@ -1,23 +0,0 @@
(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;w<l.length;w++)h(l[w]);return h})({1:[function(A,x,z){if(!l)var l={map:function(h,g){var l={};return g?
h.map(function(h,b){l.index=b;return g.call(l,h)}):h.slice()},naturalOrder:function(h,g){return h<g?-1:h>g?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<<d)+a}function g(f){function c(){a.sort(f);b=!0}var a=[],b=!1;return{push:function(c){a.push(c);b=!1},peek:function(f){b||c();void 0===f&&(f=a.length-1);return a[f]},
pop:function(){b||c();return a.pop()},size:function(){return a.length},map:function(c){return a.map(c)},debug:function(){b||c();return a}}}function w(f,c,a,b,m,e,q){this.r1=f;this.r2=c;this.g1=a;this.g2=b;this.b1=m;this.b2=e;this.histo=q}function p(){this.vboxes=new g(function(f,c){return l.naturalOrder(f.vbox.count()*f.vbox.volume(),c.vbox.count()*c.vbox.volume())})}function b(f){var c=Array(1<<3*d),a,b,m,r;f.forEach(function(f){b=f[0]>>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;h<a?a=h:h>b&&(b=h);k<m?m=k:k>d&&(d=k);l<q?q=l:l>n&&(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;b<c.size();b++)if(c.peek(b).vbox.contains(a))return c.peek(b).color;return this.nearest(a)},nearest:function(a){for(var c=this.vboxes,b,n,d,e=0;e<c.size();e++)if(n=Math.sqrt(Math.pow(a[0]-c.peek(e).color[0],2)+Math.pow(a[1]-
c.peek(e).color[1],2)+Math.pow(a[2]-c.peek(e).color[2],2)),n<b||void 0===b)b=n,d=c.peek(e).color;return d},forcebw:function(){var a=this.vboxes;a.sort(function(a,b){return l.naturalOrder(l.sum(a.color),l.sum(b.color))});var b=a[0].color;5>b[0]&&5>b[1]&&5>b[2]&&(a[0].color=[0,0,0]);var b=a.length-1,n=a[b].color;251<n[0]&&251<n[1]&&251<n[2]&&(a[b].color=[255,255,255])}};return{quantize:function(d,c){function e(a,b){for(var c=1,d=0,f;1E3>d;)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(1E3<d++)break}else a.push(f),d++}if(!d.length||2>c||256<c)return!1;var h=b(d),m=0;h.forEach(function(){m++});var r=a(d,h),q=new g(function(a,b){return l.naturalOrder(a.count(),b.count())});q.push(r);e(q,0.75*c);for(r=new g(function(a,b){return l.naturalOrder(a.count()*a.volume(),b.count()*b.volume())});q.size();)r.push(q.pop());e(r,c-r.size());for(q=new p;r.size();)q.push(r.pop());return q}}}();x.exports=A.quantize},{}],2:[function(A,x,z){(function(){var l,
h,g,w=function(b,a){return function(){return b.apply(a,arguments)}},p=[].slice;window.Swatch=h=function(){function b(a,b){this.rgb=a;this.population=b}b.prototype.hsl=void 0;b.prototype.rgb=void 0;b.prototype.population=1;b.yiq=0;b.prototype.getHsl=function(){return this.hsl?this.hsl:this.hsl=g.rgbToHsl(this.rgb[0],this.rgb[1],this.rgb[2])};b.prototype.getPopulation=function(){return this.population};b.prototype.getRgb=function(){return this.rgb};b.prototype.getHex=function(){return"#"+(16777216+
(this.rgb[0]<<16)+(this.rgb[1]<<8)+this.rgb[2]).toString(16).slice(1,7)};b.prototype.getTitleTextColor=function(){this._ensureTextColors();return 200>this.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<m;)e=4*g,q=r[e+0],c=r[e+1],f=r[e+2],e=r[e+3],125<=e&&(250<q&&250<c&&250<f||a.push([q,c,f])),g+=d;this._swatches=this.quantize(a,b).vboxes.map(function(a){return function(a){return new h(a.color,a.vbox.count())}}(this));this.maxPopulation=this.findMaxPopulation;this.generateVarationColors();this.generateEmptySwatches();p.removeCanvas()}b.prototype.quantize=
A("quantize");b.prototype._swatches=[];b.prototype.TARGET_DARK_LUMA=0.26;b.prototype.MAX_DARK_LUMA=0.45;b.prototype.MIN_LIGHT_LUMA=0.55;b.prototype.TARGET_LIGHT_LUMA=0.74;b.prototype.MIN_NORMAL_LUMA=0.3;b.prototype.TARGET_NORMAL_LUMA=0.5;b.prototype.MAX_NORMAL_LUMA=0.7;b.prototype.TARGET_MUTED_SATURATION=0.3;b.prototype.MAX_MUTED_SATURATION=0.4;b.prototype.TARGET_VIBRANT_SATURATION=1;b.prototype.MIN_VIBRANT_SATURATION=0.35;b.prototype.WEIGHT_SATURATION=3;b.prototype.WEIGHT_LUMA=6;b.prototype.WEIGHT_POPULATION=
1;b.prototype.VibrantSwatch=void 0;b.prototype.MutedSwatch=void 0;b.prototype.DarkVibrantSwatch=void 0;b.prototype.DarkMutedSwatch=void 0;b.prototype.LightVibrantSwatch=void 0;b.prototype.LightMutedSwatch=void 0;b.prototype.HighestPopulation=0;b.prototype.generateVarationColors=function(){this.VibrantSwatch=this.findColorVariation(this.TARGET_NORMAL_LUMA,this.MIN_NORMAL_LUMA,this.MAX_NORMAL_LUMA,this.TARGET_VIBRANT_SATURATION,this.MIN_VIBRANT_SATURATION,1);this.LightVibrantSwatch=this.findColorVariation(this.TARGET_LIGHT_LUMA,
this.MIN_LIGHT_LUMA,1,this.TARGET_VIBRANT_SATURATION,this.MIN_VIBRANT_SATURATION,1);this.DarkVibrantSwatch=this.findColorVariation(this.TARGET_DARK_LUMA,0,this.MAX_DARK_LUMA,this.TARGET_VIBRANT_SATURATION,this.MIN_VIBRANT_SATURATION,1);this.MutedSwatch=this.findColorVariation(this.TARGET_NORMAL_LUMA,this.MIN_NORMAL_LUMA,this.MAX_NORMAL_LUMA,this.TARGET_MUTED_SATURATION,0,this.MAX_MUTED_SATURATION);this.LightMutedSwatch=this.findColorVariation(this.TARGET_LIGHT_LUMA,this.MIN_LIGHT_LUMA,1,this.TARGET_MUTED_SATURATION,
0,this.MAX_MUTED_SATURATION);return this.DarkMutedSwatch=this.findColorVariation(this.TARGET_DARK_LUMA,0,this.MAX_DARK_LUMA,this.TARGET_MUTED_SATURATION,0,this.MAX_MUTED_SATURATION)};b.prototype.generateEmptySwatches=function(){var a;void 0===this.VibrantSwatch&&void 0!==this.DarkVibrantSwatch&&(a=this.DarkVibrantSwatch.getHsl(),a[2]=this.TARGET_NORMAL_LUMA,this.VibrantSwatch=new h(b.hslToRgb(a[0],a[1],a[2]),0));if(void 0===this.DarkVibrantSwatch&&void 0!==this.VibrantSwatch)return a=this.VibrantSwatch.getHsl(),
a[2]=this.TARGET_DARK_LUMA,this.DarkVibrantSwatch=new h(b.hslToRgb(a[0],a[1],a[2]),0)};b.prototype.findMaxPopulation=function(){var a,b,d,e,f;d=0;e=this._swatches;a=0;for(b=e.length;a<b;a++)f=e[a],d=Math.max(d,f.getPopulation());return d};b.prototype.findColorVariation=function(a,b,d,e,f,c){var g,h,m,l,q,p,s,k;l=void 0;q=0;p=this._swatches;g=0;for(h=p.length;g<h;g++)if(k=p[g],s=k.getHsl()[1],m=k.getHsl()[2],s>=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;a<f.length;)e=f[a],c=f[a+1],b+=e*c,d+=c,a+=2;return b/d};b.prototype.swatches=
function(){return{Vibrant:this.VibrantSwatch,Muted:this.MutedSwatch,DarkVibrant:this.DarkVibrantSwatch,DarkMuted:this.DarkMutedSwatch,LightVibrant:this.LightVibrantSwatch,LightMuted:this.LightMuted}};b.prototype.isAlreadySelected=function(a){return this.VibrantSwatch===a||this.DarkVibrantSwatch===a||this.LightVibrantSwatch===a||this.MutedSwatch===a||this.DarkMutedSwatch===a||this.LightMutedSwatch===a};b.rgbToHsl=function(a,b,d){var e,f,c,g,h;a/=255;b/=255;d/=255;g=Math.max(a,b,d);h=Math.min(a,b,d);
f=void 0;c=(g+h)/2;if(g===h)f=h=0;else{e=g-h;h=0.5<c?e/(2-g-h):e/(g+h);switch(g){case a:f=(b-d)/e+(b<d?6:0);break;case b:f=(d-a)/e+2;break;case d:f=(a-b)/e+4}f/=6}return[f,h,c]};b.hslToRgb=function(a,b,d){var e,f,c;e=f=c=void 0;e=function(a,b,c){0>c&&(c+=1);1<c&&(c-=1);return c<1/6?a+6*(b-a)*c:0.5>c?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]);

View file

@ -1,453 +0,0 @@
let current = "2.6.0";
/* Config settings */
DribbblishShared.config.register({
area: "Animations & Transitions",
type: "slider",
key: "fadeDuration",
name: "Color Fade Duration",
description: "Select the duration of the color fading transition",
defaultValue: 0.5,
data: {
min: 0,
max: 10,
step: 0.1,
suffix: "s"
},
onChange: (val) => document.documentElement.style.setProperty("--song-transition-speed", val + "s")
});
// waitForElement because Spicetify is not initialized at startup
waitForElement(["#main"], () => {
DribbblishShared.config.register({
area: { name: "About", order: 999 },
type: "button",
key: "aboutDribbblish",
name: "Info",
description: `
OS: ${capitalizeFirstLetter(Spicetify.Platform.PlatformData.os_name)} v${Spicetify.Platform.PlatformData.os_version}
Spotify: v${Spicetify.Platform.PlatformData.event_sender_context_information?.client_version_string ?? Spicetify.Platform.PlatformData.client_version_triple}
Dribbblish: v${current}
`,
data: "Copy",
onChange: (val) => {
copyToClipboard(DribbblishShared.config.getOptions("aboutDribbblish").description);
Spicetify.showNotification("Copied Versions");
}
});
});
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function copyToClipboard(text) {
var input = document.createElement("textarea");
input.style.display = "fixed";
input.innerHTML = text;
document.body.appendChild(input);
input.select();
var result = document.execCommand("copy");
document.body.removeChild(input);
return result;
}
/* js */
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));
}
function parseComputedStyleColor(col) {
if (col.startsWith("#")) return col;
if (col.startsWith("rgb("))
return rgbToHex(
col
.replace(/rgb|(|)/g, "")
.split(",")
.map((part) => Number(part.trim()))
);
}
// `parseComputedStyleColor()` beacuse "--spice-sidebar" is `rgb()`
let textColor = parseComputedStyleColor(getComputedStyle(document.documentElement).getPropertyValue("--spice-text"));
let textColorBg = parseComputedStyleColor(getComputedStyle(document.documentElement).getPropertyValue("--spice-main"));
let sidebarColor = parseComputedStyleColor(getComputedStyle(document.documentElement).getPropertyValue("--spice-sidebar"));
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(textColorBg);
document.documentElement.style.setProperty("--is_light", setDark ? 0 : 1);
textColorBg = setDark ? "#0A0A0A" : "#FAFAFA";
setRootColor("main", textColorBg);
setRootColor("player", textColorBg);
setRootColor("card", setDark ? "#040404" : "#ECECEC");
setRootColor("subtext", setDark ? "#EAEAEA" : "#3D3D3D");
setRootColor("notification", setDark ? "#303030" : "#DDDDDD");
updateColors(textColor, sidebarColor, false);
}
function checkDarkLightMode(colors) {
const theme = DribbblishShared.config.get("theme");
if (theme == 2) {
// Based on Time
const start = 60 * parseInt(DribbblishShared.config.get("darkModeOnTime").split(":")[0]) + parseInt(DribbblishShared.config.get("darkModeOnTime").split(":")[1]);
const end = 60 * parseInt(DribbblishShared.config.get("darkModeOffTime").split(":")[0]) + parseInt(DribbblishShared.config.get("darkModeOffTime").split(":")[1]);
const now = new Date();
const time = 60 * now.getHours() + now.getMinutes();
if (end < start) dark = start <= time || time < end;
else dark = start <= time && time < end;
toggleDark(dark);
} else if (theme == 3) {
// Based on Color
if (colors && colors.length > 0) toggleDark(isLight(colors[0]));
}
}
// Run every Minute to check time and set dark / light mode
setInterval(checkDarkLightMode, 60000);
DribbblishShared.config.register({
area: "Theme",
type: "checkbox",
key: "dynamicColors",
name: "Dynamic",
description: "If the Theme's Color should be extracted from Albumart",
defaultValue: true,
onChange: (val) => updateColors(),
showChildren: (val) => !val,
children: [
{
type: "color",
key: "colorOverride",
name: "Color",
description: "The Color of the Theme",
defaultValue: "#1ed760",
fireInitialChange: false,
onChange: (val) => updateColors()
}
]
});
DribbblishShared.config.register({
area: "Theme",
type: "select",
data: ["Dark", "Light", "Based on Time", "Based on Color"],
key: "theme",
name: "Theme",
description: "Select Dark / Bright mode",
defaultValue: 0,
showChildren: (val) => {
if (val == 2) return ["darkModeOnTime", "darkModeOffTime"];
//if (val == 3) return [""];
return false;
},
onChange: (val) => {
switch (val) {
case 0:
toggleDark(true);
break;
case 1:
toggleDark(false);
break;
case 2:
checkDarkLightMode();
break;
case 3:
checkDarkLightMode();
break;
}
},
children: [
{
type: "time",
key: "darkModeOnTime",
name: "Dark Mode On Time",
description: "Beginning of Dark mode time",
defaultValue: "20:00",
fireInitialChange: false,
onChange: checkDarkLightMode
},
{
type: "time",
key: "darkModeOffTime",
name: "Dark Mode Off Time",
description: "End of Dark mode time",
defaultValue: "06:00",
fireInitialChange: false,
onChange: checkDarkLightMode
}
]
});
var currentColor;
var currentSideColor;
function updateColors(textColHex, sideColHex, checkDarkMode = true) {
if (textColHex && sideColHex) {
currentColor = textColHex;
currentSideColor = sideColHex;
} else {
if (!(currentColor && currentSideColor)) return; // If `updateColors()` is called early these vars are undefined and would break
textColHex = currentColor;
sideColHex = currentSideColor;
}
if (!DribbblishShared.config.get("dynamicColors")) {
const col = DribbblishShared.config.get("colorOverride");
textColHex = col;
sideColHex = col;
}
let isLightBg = isLight(textColorBg);
if (isLightBg) textColHex = LightenDarkenColor(textColHex, -15); // vibrant color is always too bright for white bg mode
let darkColHex = LightenDarkenColor(textColHex, isLightBg ? 12 : -20);
let darkerColHex = LightenDarkenColor(textColHex, isLightBg ? 30 : -40);
let buttonBgColHex = setLightness(textColHex, isLightBg ? 0.9 : 0.14);
setRootColor("text", textColHex);
setRootColor("button", darkerColHex);
setRootColor("button-active", darkColHex);
setRootColor("selected-row", darkerColHex);
setRootColor("tab-active", buttonBgColHex);
setRootColor("button-disabled", buttonBgColHex);
setRootColor("sidebar", sideColHex);
if (checkDarkMode) checkDarkLightMode([textColHex, sideColHex]);
}
let nearArtistSpanText = "";
let coverListenerInstalled = false;
async function songchange() {
try {
// warning popup
if (Spicetify.Platform.PlatformData.client_version_triple < "1.1.68") Spicetify.showNotification(`Your version of Spotify ${Spicetify.Platform.PlatformData.client_version_triple}) is un-supported`);
} catch (err) {
console.error(err);
}
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";
textColor = "#509bf5";
updateColors(textColor, textColor);
coverListenerInstalled = false;
}
if (!coverListenerInstalled) hookCoverChange(true);
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 = '<a title="' + Spicetify.Player.data.track.metadata.album_title + '" href="' + album_uri + '" data-uri="' + album_uri + '" data-interaction-target="album-name" class="tl-cell__content">' + Spicetify.Player.data.track.metadata.album_title + "</a>";
nearArtistSpanText = album_link + " • " + album_date;
} else if (Spicetify.Player.data.track.uri.includes("spotify:episode")) {
// podcast
bgImage = bgImage.replace("spotify:image:", "https://i.scdn.co/image/");
nearArtistSpanText = Spicetify.Player.data.track.metadata.album_title;
} else if (Spicetify.Player.data.track.metadata.is_local == "true") {
// local file
nearArtistSpanText = Spicetify.Player.data.track.metadata.album_title;
} else if (Spicetify.Player.data.track.provider == "ad") {
// ad
nearArtistSpanText = "advertisement";
coverListenerInstalled = false;
return;
} else {
// When clicking a song from the homepage, songChange is fired with half empty metadata
// todo: retry only once?
setTimeout(songchange, 200);
}
if (document.querySelector("#main-trackInfo-year") === null) {
waitForElement([".main-trackInfo-container"], (queries) => {
nearArtistSpan = document.createElement("div");
nearArtistSpan.id = "main-trackInfo-year";
nearArtistSpan.classList.add("main-trackInfo-artists", "ellipsis-one-line", "main-type-finale");
nearArtistSpan.innerHTML = nearArtistSpanText;
queries[0].append(nearArtistSpan);
});
} else {
nearArtistSpan.innerHTML = nearArtistSpanText;
}
document.documentElement.style.setProperty("--image_url", 'url("' + bgImage + '")');
}
Spicetify.Player.addEventListener("songchange", songchange);
function pickCoverColor(img) {
if (!img.currentSrc.startsWith("spotify:")) return;
var swatches = new Vibrant(img, 5).swatches();
lightCols = ["Vibrant", "DarkVibrant", "Muted", "LightVibrant"];
darkCols = ["Vibrant", "LightVibrant", "Muted", "DarkVibrant"];
mainCols = isLight(textColorBg) ? lightCols : darkCols;
textColor = "#509bf5";
for (var col in mainCols)
if (swatches[mainCols[col]]) {
textColor = swatches[mainCols[col]].getHex();
break;
}
sidebarColor = "#509bf5";
for (var col in lightCols)
if (swatches[lightCols[col]]) {
sidebarColor = swatches[lightCols[col]].getHex();
break;
}
updateColors(textColor, sidebarColor);
}
waitForElement([".main-nowPlayingBar-left"], (queries) => {
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.removedNodes.length > 0) coverListenerInstalled = false;
});
});
observer.observe(queries[0], { childList: true });
});
function hookCoverChange(pick) {
waitForElement([".cover-art-image"], (queries) => {
coverListenerInstalled = true;
var elem = queries.slice(-1)[0];
if (pick && elem.complete && elem.naturalHeight !== 0) pickCoverColor(elem);
elem.addEventListener("load", function () {
try {
pickCoverColor(elem);
} catch (error) {
console.error(error);
setTimeout(pickCoverColor, 300, elem);
}
});
});
}
(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) {
upd = document.createElement("div");
upd.innerText = `Theme UPD v${data.tag_name} avail.`;
upd.classList.add("ellipsis-one-line", "main-type-finale");
upd.setAttribute("title", `Changes: ${data.name}`);
upd.style.setProperty("color", "var(--spice-button-active)");
document.querySelector(".main-userWidget-box").append(upd);
document.querySelector(".main-userWidget-box").classList.add("update-avail");
new Spicetify.Menu.Item("Update Dribbblish", false, () => window.open("https://github.com/JulienMaille/dribbblish-dynamic-theme/blob/main/README.md#install--update", "_blank")).register();
}
})
.catch((err) => {
// Do something for an error here
console.error(err);
});
})();
document.documentElement.style.setProperty("--warning_message", " ");

View file

@ -45,6 +45,7 @@ if ($PSVersionTable.PSVersion.Major -gt $PSMinVersion) {
Write-Done
$version = ($latest_release_json | ConvertFrom-Json).tag_name -replace "v", ""
$download_uri = ($latest_release_json | ConvertFrom-Json).assets[0].browser_download_url
}
# Check ~\spicetify-cli\Themes directory already exists
@ -57,7 +58,6 @@ if ($PSVersionTable.PSVersion.Major -gt $PSMinVersion) {
# Download release.
$zip_file = "${sp_dir}\${version}.zip"
$download_uri = "https://github.com/JulienMaille/dribbblish-dynamic-theme/archive/refs/tags/${version}.zip"
Write-Part "DOWNLOADING "; Write-Emphasized $download_uri
Invoke-WebRequest -Uri $download_uri -UseBasicParsing -OutFile $zip_file
Write-Done
@ -65,7 +65,7 @@ if ($PSVersionTable.PSVersion.Major -gt $PSMinVersion) {
# Extract theme from .zip file.
Write-Part "EXTRACTING "; Write-Emphasized $zip_file
Write-Part " into "; Write-Emphasized ${sp_dir};
Expand-Archive -Path $zip_file -DestinationPath $sp_dir -Force
Expand-Archive -Path $zip_file -DestinationPath "${sp_dir}\dribbblish-dynamic-theme-${version}" -Force
Write-Done
# Remove .zip file.
@ -90,11 +90,9 @@ if ($PSVersionTable.PSVersion.Major -gt $PSMinVersion) {
# Installing.
Write-Part "INSTALLING";
cd $sp_dot_dir
Copy-Item dribbblish.js ..\..\Extensions
Copy-Item dribbblish-dynamic.js ..\..\Extensions
Copy-Item Vibrant.min.js ..\..\Extensions
spicetify config extensions default-dynamic.js-
spicetify config extensions dribbblish.js extensions dribbblish-dynamic.js extensions Vibrant.min.js
spicetify config extensions default-dynamic.js- extensions dribbblish-dynamic.js- extensions dribbblish.js- extensions Vibrant.min.js-
spicetify config extensions dribbblish-dynamic.js
spicetify config current_theme DribbblishDynamic
spicetify config color_scheme base
spicetify config inject_css 1 replace_colors 1 overwrite_assets 1

View file

@ -10,13 +10,14 @@ if [ $# -eq 0 ]; then
version=$( command curl -sSf "$latest_release_uri" |
command grep -Eo "tag_name\": .*" |
command grep -Eo "[0-9.]+" )
download_uri=$( command curl -sSf "$latest_release_uri" |
command grep -Eo "browser_download_url\": .*" |
command grep -Eo "http.*?\.zip" )
if [ ! "$version" ]; then exit 1; fi
else
version="${1}"
fi
download_uri="https://github.com/JulienMaille/dribbblish-dynamic-theme/archive/refs/tags/${version}.zip"
spicetify_install="${SPICETIFY_INSTALL:-$HOME/spicetify-cli/Themes}"
if [ ! -d "$spicetify_install" ]; then
@ -31,7 +32,7 @@ curl --fail --location --progress-bar --output "$tar_file" "$download_uri"
cd "$spicetify_install"
echo "EXTRACTING $tar_file"
unzip -o "$tar_file"
unzip -d "$spicetify_install/dribbblish-dynamic-theme-${version}" -o "$tar_file"
echo "REMOVING $tar_file"
rm "$tar_file"
@ -49,10 +50,9 @@ cp -rf "$spicetify_install/dribbblish-dynamic-theme-${version}/." "$sp_dot_dir"
echo "INSTALLING"
cd "$(dirname "$(spicetify -c)")/Themes/DribbblishDynamic"
mkdir -p ../../Extensions
cp dribbblish.js ../../Extensions/.
cp dribbblish-dynamic.js ../../Extensions/.
cp Vibrant.min.js ../../Extensions/.
spicetify config extensions dribbblish.js extensions dribbblish-dynamic.js extensions Vibrant.min.js
spicetify config extensions default-dynamic.js- extensions dribbblish-dynamic.js- extensions dribbblish.js- extensions Vibrant.min.js-
spicetify config extensions dribbblish-dynamic.js
spicetify config current_theme DribbblishDynamic
spicetify config color_scheme base
spicetify config inject_css 1 replace_colors 1 overwrite_assets 1

19
package.json Normal file
View file

@ -0,0 +1,19 @@
{
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^9.0.1",
"css-minimizer-webpack-plugin": "^3.1.1",
"node-sass": "^6.0.1",
"sass-loader": "^12.2.0",
"webpack": "^5.58.2",
"webpack-cli": "^4.9.0"
},
"scripts": {
"build": "webpack --mode=development"
},
"dependencies": {
"chroma-js": "^2.1.2",
"jquery": "^3.6.0",
"node-vibrant": "3.1.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View file

@ -1,162 +1,162 @@
[base]
text = FFFFFF
subtext = F0F0F0
sidebar-text = FFFFFF
main = 000000
sidebar = 1ed760
player = 000000
card = 000000
shadow = 202020
selected-row = 797979
button = 1ed760
button-active = 1ed760
button-disabled = 535353
tab-active = 166632
notification = 1db954
notification-error = e22134
misc = BFBFBF
[white]
text = 363636
subtext = 3D3D3D
sidebar-text = FFF9F4
main = FFF9F4
sidebar = FFA789
player = FFF9F4
card = FFF9F4
shadow = d3d3d3
selected-row = 6D6D6D
button = ff8367
button-active = ff8367
button-disabled = 535353
tab-active = ffdace
notification = FFA789
notification-error = e22134
misc = BFBFBF
[dark]
text = F0F0F0
subtext = F0F0F0
sidebar-text = 0a0e14
main = 0a0e14
sidebar = C2D935
player = 0a0e14
card = 0a0e14
shadow = 202020
selected-row = DEDEDE
button = C2D935
button-active = C2D935
button-disabled = 535353
tab-active = 727d2b
notification = C2D935
notification-error = e22134
misc = BFBFBF
[dracula]
text = f8f8f2
subtext = f8f8f2
sidebar-text = F0F0F0
main = 44475a
sidebar = 6272a4
player = 44475a
card = 6272a4
shadow = 000000
selected-row = bd93f9
button = ffb86c
button-active = 8be9fd
button-disabled = 535353
tab-active = 6272a4
notification = bd93f9
notification-error = e22134
misc = BFBFBF
[nord-light]
text = 2e3440
subtext = 3b4252
sidebar-text = ECEFF4
main = ECEFF4
sidebar = 5E81AC
player = ECEFF4
card = ebcb8b
shadow = eceff4
selected-row = 4c566a
button = 81a1c1
button-active = 81a1c1
button-disabled = c0c0c0
tab-active = ebcb8b
notification = a3be8c
notification-error = bf616a
misc = BFBFBF
[nord-dark]
text = ECEFF4
subtext = E5E9F0
sidebar-text = 434c5e
main = 2e3440
sidebar = 88C0D0
player = 2e3440
card = 2e3440
shadow = 2E3440
selected-row = D8DEE9
button = 81A1C1
button-active = 81A1C1
button-disabled = 434C5E
tab-active = 434C5E
notification = A3BE8C
notification-error = BF616A
misc = BFBFBF
[purple]
text = f1eaff
subtext = f1eaff
sidebar-text = e0d0ff
main = 0A0E14
sidebar = 6F3C89
player = 0A0E14
card = 0A0E14
shadow = 3a2645
selected-row = EBDFFF
button = c76af6
button-active = 6F3C89
button-disabled = 535353
tab-active = 58306D
notification = ff9e00
notification-error = f61379
misc = DEDEDE
[samourai]
text = ebdbb2
subtext = ebdbb2
sidebar-text = 461217
main = 461217
sidebar = ebdbb2
player = 461217
card = 461217
shadow = 3a2645
selected-row = 909090
button = e7a52d
button-active = e7a52d
button-disabled = 535353
tab-active = e7a52d
notification = e7a52d
notification-error = e22134
misc = BFBFBF
[beach-sunset]
text = FFFFFF
subtext = F0F0F0
sidebar-text = F0F0F0
main = 262626
sidebar = bd3e3e
player = 262626
card = 262626
shadow = 000000
selected-row = d1d6e2
button = f1a84f
button-active = c98430
button-disabled = 535353
tab-active = f1a84f
notification = c98430
notification-error = e22134
[base]
text = FFFFFF
subtext = F0F0F0
sidebar-text = FFFFFF
main = 000000
sidebar = 1ed760
player = 000000
card = 000000
shadow = 202020
selected-row = 797979
button = 1ed760
button-active = 1ed760
button-disabled = 535353
tab-active = 166632
notification = 1db954
notification-error = e22134
misc = BFBFBF
[white]
text = 363636
subtext = 3D3D3D
sidebar-text = FFF9F4
main = FFF9F4
sidebar = FFA789
player = FFF9F4
card = FFF9F4
shadow = d3d3d3
selected-row = 6D6D6D
button = ff8367
button-active = ff8367
button-disabled = 535353
tab-active = ffdace
notification = FFA789
notification-error = e22134
misc = BFBFBF
[dark]
text = F0F0F0
subtext = F0F0F0
sidebar-text = 0a0e14
main = 0a0e14
sidebar = C2D935
player = 0a0e14
card = 0a0e14
shadow = 202020
selected-row = DEDEDE
button = C2D935
button-active = C2D935
button-disabled = 535353
tab-active = 727d2b
notification = C2D935
notification-error = e22134
misc = BFBFBF
[dracula]
text = f8f8f2
subtext = f8f8f2
sidebar-text = F0F0F0
main = 44475a
sidebar = 6272a4
player = 44475a
card = 6272a4
shadow = 000000
selected-row = bd93f9
button = ffb86c
button-active = 8be9fd
button-disabled = 535353
tab-active = 6272a4
notification = bd93f9
notification-error = e22134
misc = BFBFBF
[nord-light]
text = 2e3440
subtext = 3b4252
sidebar-text = ECEFF4
main = ECEFF4
sidebar = 5E81AC
player = ECEFF4
card = ebcb8b
shadow = eceff4
selected-row = 4c566a
button = 81a1c1
button-active = 81a1c1
button-disabled = c0c0c0
tab-active = ebcb8b
notification = a3be8c
notification-error = bf616a
misc = BFBFBF
[nord-dark]
text = ECEFF4
subtext = E5E9F0
sidebar-text = 434c5e
main = 2e3440
sidebar = 88C0D0
player = 2e3440
card = 2e3440
shadow = 2E3440
selected-row = D8DEE9
button = 81A1C1
button-active = 81A1C1
button-disabled = 434C5E
tab-active = 434C5E
notification = A3BE8C
notification-error = BF616A
misc = BFBFBF
[purple]
text = f1eaff
subtext = f1eaff
sidebar-text = e0d0ff
main = 0A0E14
sidebar = 6F3C89
player = 0A0E14
card = 0A0E14
shadow = 3a2645
selected-row = EBDFFF
button = c76af6
button-active = 6F3C89
button-disabled = 535353
tab-active = 58306D
notification = ff9e00
notification-error = f61379
misc = DEDEDE
[samourai]
text = ebdbb2
subtext = ebdbb2
sidebar-text = 461217
main = 461217
sidebar = ebdbb2
player = 461217
card = 461217
shadow = 3a2645
selected-row = 909090
button = e7a52d
button-active = e7a52d
button-disabled = 535353
tab-active = e7a52d
notification = e7a52d
notification-error = e22134
misc = BFBFBF
[beach-sunset]
text = FFFFFF
subtext = F0F0F0
sidebar-text = F0F0F0
main = 262626
sidebar = bd3e3e
player = 262626
card = 262626
shadow = 000000
selected-row = d1d6e2
button = f1a84f
button-active = c98430
button-disabled = 535353
tab-active = f1a84f
notification = c98430
notification-error = e22134
misc = BFBFBF

View file

@ -1,6 +1,4 @@
// Hide popover message
// document.getElementById("popover-container").style.height = 0;
class ConfigMenu {
export default class ConfigMenu {
/**
* @typedef {Object} DribbblishConfigItem
* @property {"checkbox" | "select" | "button" | "slider" | "number" | "text" | "time" | "color"} type
@ -29,17 +27,20 @@ class ConfigMenu {
/**
* @callback showChildren
* @this {DribbblishConfigItem}
* @param {any} value
* @returns {Boolean | String[]}
*/
/**
* @callback onAppended
* @this {DribbblishConfigItem}
* @returns {void}
*/
/**
* @callback onChange
* @this {DribbblishConfigItem}
* @param {any} value
* @returns {void}
*/
@ -49,7 +50,7 @@ class ConfigMenu {
constructor() {
this.#config = {};
this.configButton = new Spicetify.Menu.Item("Dribbblish Settings", false, () => DribbblishShared.config.open());
this.configButton = new Spicetify.Menu.Item("Dribbblish Settings", false, () => this.open());
this.configButton.register();
const container = document.createElement("div");
@ -66,8 +67,8 @@ class ConfigMenu {
`;
document.body.appendChild(container);
document.querySelector(".dribbblish-config-close").addEventListener("click", () => DribbblishShared.config.close());
document.querySelector(".dribbblish-config-backdrop").addEventListener("click", () => DribbblishShared.config.close());
document.querySelector(".dribbblish-config-close").addEventListener("click", () => this.close());
document.querySelector(".dribbblish-config-backdrop").addEventListener("click", () => this.close());
}
open() {
@ -138,8 +139,8 @@ class ConfigMenu {
.join("\n");
options._onChange = options.onChange;
options.onChange = (val) => {
options._onChange(val);
const show = options.showChildren(val);
options._onChange.call(options, val);
const show = options.showChildren.call(options, val);
options.children.forEach((child) => this.setHidden(child.key, Array.isArray(show) ? !show.includes(child.key) : !show));
};
options.children = options.children.map((child) => {
@ -147,7 +148,6 @@ class ConfigMenu {
});
this.#config[options.key] = options;
this.#config[options.key].value = localStorage.getItem(`dribbblish:config:${options.key}`) ?? JSON.stringify(options.defaultValue);
if (options.type == "checkbox") {
const input = /* html */ `
@ -165,17 +165,19 @@ class ConfigMenu {
} else if (options.type == "select") {
// Validate
const val = this.get(options.key);
if (val < 0 || val > options.data.length - 1) this.set(options.key);
if (!Object.keys(options.data).includes(val)) this.reset(options.key);
const input = /* html */ `
<select class="main-dropDown-dropDown" id="dribbblish-config-input-${options.key}">
${options.data.map((option, i) => `<option value="${i}"${this.get(options.key) == i ? " selected" : ""}>${option}</option>`).join("")}
${Object.entries(options.data)
.map(([key, name]) => `<option value="${key}"${this.get(options.key) == key ? " selected" : ""}>${name}</option>`)
.join("")}
</select>
`;
this.addInputHTML({ ...options, input });
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("change", (e) => {
this.set(options.key, Number(e.target.value));
this.set(options.key, e.target.value);
options.onChange(this.get(options.key));
});
} else if (options.type == "button") {
@ -289,7 +291,7 @@ class ConfigMenu {
options.children.forEach((child) => this.register(child));
options.onAppended();
options.onAppended.call(options);
if (options.fireInitialChange) options.onChange(this.get(options.key));
}
@ -332,9 +334,12 @@ class ConfigMenu {
* @returns {any}
*/
get(key, defaultValueOverride) {
const val = JSON.parse(this.#config[key].value ?? null); // Turn undefined into null because `JSON.parse()` dosen't like undefined
if (val == null) return defaultValueOverride ?? this.#config[key].defaultValue;
return val;
const val = JSON.parse(this.#config[key]?.storageCache ?? localStorage.getItem(`dribbblish:config:${key}`) ?? null); // Turn undefined into null because `JSON.parse()` dosen't like undefined
if (val == null || val?.type != this.#config[key]?.type) {
localStorage.removeItem(`dribbblish:config:${key}`);
return defaultValueOverride ?? this.#config[key].defaultValue;
}
return val.value;
}
/**
@ -343,10 +348,20 @@ class ConfigMenu {
* @param {any} val
*/
set(key, val) {
this.#config[key].value = JSON.stringify(val);
val = { type: this.#config[key].type, value: val ?? this.#config[key].defaultValue };
this.#config[key].storageCache = JSON.stringify(val);
localStorage.setItem(`dribbblish:config:${key}`, JSON.stringify(val));
}
/**
*
* @param {String} key
*/
reset(key) {
delete this.#config[key].storageCache;
localStorage.removeItem(`dribbblish:config:${key}`);
}
/**
*
* @param {String} key
@ -361,334 +376,3 @@ class ConfigMenu {
return this.#config[key];
}
}
class _DribbblishShared {
constructor() {
this.config = new ConfigMenu();
}
}
const DribbblishShared = new _DribbblishShared();
DribbblishShared.config.register({
type: "checkbox",
key: "rightBigCover",
name: "Right expanded cover",
description: "Have the expanded cover Image on the right instead of on the left",
defaultValue: true,
onChange: (val) => {
if (val) {
document.documentElement.classList.add("right-expanded-cover");
} else {
document.documentElement.classList.remove("right-expanded-cover");
}
}
});
DribbblishShared.config.register({
type: "checkbox",
key: "roundSidebarIcons",
name: "Round Sidebar Icons",
description: "If the Sidebar Icons should be round instead of square",
defaultValue: false,
onChange: (val) => document.documentElement.style.setProperty("--sidebar-icons-border-radius", val ? "50%" : "var(--image-radius)")
});
DribbblishShared.config.register({
area: "Animations & Transitions",
type: "checkbox",
key: "sidebarHoverAnimation",
name: "Sidebar Hover Animation",
description: "If the Sidebar Icons should have an animated background on hover",
defaultValue: true,
onChange: (val) => document.documentElement.style.setProperty("--sidebar-icons-hover-animation", val ? "1" : "0")
});
waitForElement(["#main"], () => {
DribbblishShared.config.register({
type: "select",
data: ["None", "None (With Top Padding)", "Solid", "Transparent"],
key: "winTopBar",
name: "Windows Top Bar",
description: "Have different top Bars (or none at all)",
defaultValue: 0,
onChange: (val) => {
switch (val) {
case 0:
document.getElementById("main").setAttribute("top-bar", "none");
break;
case 1:
document.getElementById("main").setAttribute("top-bar", "none-padding");
break;
case 2:
document.getElementById("main").setAttribute("top-bar", "solid");
break;
case 3:
document.getElementById("main").setAttribute("top-bar", "transparent");
break;
}
}
});
DribbblishShared.config.register({
type: "select",
data: ["Dribbblish", "Spotify"],
key: "playerControlsStyle",
name: "Player Controls Style",
description: "Style of the Player Controls. Selecting Spotify basically changes Play / Pause back to the center",
defaultValue: 0,
onChange: (val) => {
switch (val) {
case 0:
document.getElementById("main").setAttribute("player-controls", "dribbblish");
break;
case 1:
document.getElementById("main").setAttribute("player-controls", "spotify");
break;
}
}
});
DribbblishShared.config.register({
area: "Ads",
type: "checkbox",
key: "hideAds",
name: "Hide Ads",
description: `Hide ads / premium features (see: <a href="https://github.com/Daksh777/SpotifyNoPremium">SpotifyNoPremium</a>)`,
defaultValue: false,
onAppended: () => {
document.styleSheets[0].insertRule(/* css */ `
/* Remove upgrade button*/
#main[hide-ads] .main-topBar-UpgradeButton {
display: none
}
`);
document.styleSheets[0].insertRule(/* css */ `
/* Remove upgrade to premium button in user menu */
#main[hide-ads] .main-contextMenu-menuItemButton[href="https://www.spotify.com/premium/"] {
display: none
}
`);
document.styleSheets[0].insertRule(/* css */ `
/* Remove ad placeholder in main screen */
#main[hide-ads] .main-leaderboardComponent-container {
display: none
}
`);
},
onChange: (val) => document.getElementById("main").toggleAttribute("hide-ads", val)
});
});
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);
}
}
waitForElement([`.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"]`, `.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"] li`], ([root, firstItem]) => {
const listElem = firstItem.parentElement;
root.classList.add("dribs-playlist-list");
/** Replace Playlist name with their pictures */
function loadPlaylistImage() {
for (const item of listElem.children) {
let link = item.querySelector("a");
if (!link) continue;
let [_, app, uid] = link.pathname.split("/");
let uri;
if (app === "playlist") {
uri = Spicetify.URI.playlistV2URI(uid);
} else if (app === "folder") {
const base64 = localStorage.getItem("dribbblish:folder-image:" + uid);
let img = link.querySelector("img");
if (!img) {
img = document.createElement("img");
img.classList.add("playlist-picture");
link.prepend(img);
}
img.src = base64 || "/images/tracklist-row-song-fallback.svg";
continue;
}
Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri.toURI()}/metadata`, { policy: { picture: true } }).then((res) => {
const meta = res.metadata;
let img = link.querySelector("img");
if (!img) {
img = document.createElement("img");
img.classList.add("playlist-picture");
link.prepend(img);
}
img.src = meta.picture || "/images/tracklist-row-song-fallback.svg";
});
}
}
DribbblishShared.loadPlaylistImage = loadPlaylistImage;
loadPlaylistImage();
new MutationObserver(loadPlaylistImage).observe(listElem, { childList: true });
});
waitForElement([".main-rootlist-rootlist", ".main-rootlist-wrapper > :nth-child(2) > :first-child", "#spicetify-show-list"], ([rootlist]) => {
function checkSidebarPlaylistScroll() {
const topDist = rootlist.getBoundingClientRect().top - document.querySelector("#spicetify-show-list:not(:empty), .main-rootlist-wrapper > :nth-child(2) > :first-child").getBoundingClientRect().top;
const bottomDist = document.querySelector(".main-rootlist-wrapper > :nth-child(2) > :last-child").getBoundingClientRect().bottom - rootlist.getBoundingClientRect().bottom;
rootlist.classList.remove("no-top-shadow", "no-bottom-shadow");
if (topDist < 10) rootlist.classList.add("no-top-shadow");
if (bottomDist < 10) rootlist.classList.add("no-bottom-shadow");
}
checkSidebarPlaylistScroll();
// Use Interval because scrolling takes a while and getBoundingClientRect() gets position at the moment of calling, so the interval keeps calling for 1s
let c = 0;
let interval;
rootlist.addEventListener("wheel", () => {
checkSidebarPlaylistScroll();
c = 0;
if (interval == null)
interval = setInterval(() => {
if (c > 20) {
clearInterval(interval);
interval = null;
return;
}
checkSidebarPlaylistScroll();
c++;
}, 50);
});
});
waitForElement([".Root__main-view"], ([mainView]) => {
const shadow = document.createElement("div");
shadow.id = "dribbblish-back-shadow";
mainView.prepend(shadow);
});
waitForElement([".Root__nav-bar .LayoutResizer__input, .Root__nav-bar .LayoutResizer__resize-bar input"], ([resizer]) => {
const observer = new MutationObserver(updateVariable);
observer.observe(resizer, { attributes: true, attributeFilter: ["value"] });
function updateVariable() {
let value = resizer.value;
if (value < 121) {
value = 72;
document.documentElement.classList.add("sidebar-hide-text");
} else {
document.documentElement.classList.remove("sidebar-hide-text");
}
document.documentElement.style.setProperty("--sidebar-width", value + "px");
}
updateVariable();
});
waitForElement([".Root__main-view .os-resize-observer-host"], ([resizeHost]) => {
const observer = new ResizeObserver(updateVariable);
observer.observe(resizeHost);
function updateVariable([event]) {
document.documentElement.style.setProperty("--main-view-width", event.contentRect.width + "px");
document.documentElement.style.setProperty("--main-view-height", event.contentRect.height + "px");
if (event.contentRect.width < 700) {
document.documentElement.classList.add("minimal-player");
} else {
document.documentElement.classList.remove("minimal-player");
}
if (event.contentRect.width < 550) {
document.documentElement.classList.add("extra-minimal-player");
} else {
document.documentElement.classList.remove("extra-minimal-player");
}
}
});
(function Dribbblish() {
const progBar = document.querySelector(".playback-bar");
const root = document.querySelector(".Root");
if (!Spicetify.Player.origin || !progBar || !root) {
setTimeout(Dribbblish, 300);
return;
}
const progKnob = progBar.querySelector(".progress-bar__slider");
const tooltip = document.createElement("div");
tooltip.className = "prog-tooltip";
progKnob.append(tooltip);
function updateProgTime(timeOverride) {
const newText = Spicetify.Player.formatTime(timeOverride || Spicetify.Player.getProgress()) + " / " + Spicetify.Player.formatTime(Spicetify.Player.getDuration());
// To reduce DOM Updates when the Song is Paused
if (tooltip.innerText != newText) tooltip.innerText = newText;
}
const knobPosObserver = new MutationObserver((muts) => {
const progressPercentage = Number(getComputedStyle(document.querySelector(".progress-bar")).getPropertyValue("--progress-bar-transform").replace("%", "")) / 100;
updateProgTime(Spicetify.Player.getDuration() * progressPercentage);
});
knobPosObserver.observe(document.querySelector(".progress-bar"), {
attributes: true,
attributeFilter: ["style"]
});
Spicetify.Player.addEventListener("songchange", () => updateProgTime());
updateProgTime();
Spicetify.CosmosAsync.sub("sp://connect/v1", (state) => {
const isExternal = state.devices.some((a) => a.is_active);
if (isExternal) {
root.classList.add("is-connectBarVisible");
} else {
root.classList.remove("is-connectBarVisible");
}
});
const filePickerForm = document.createElement("form");
filePickerForm.setAttribute("aria-hidden", true);
filePickerForm.innerHTML = '<input type="file" class="hidden-visually" />';
document.body.appendChild(filePickerForm);
/** @type {HTMLInputElement} */
const filePickerInput = filePickerForm.childNodes[0];
filePickerInput.accept = ["image/jpeg", "image/apng", "image/avif", "image/gif", "image/png", "image/svg+xml", "image/webp"].join(",");
filePickerInput.onchange = () => {
if (!filePickerInput.files.length) return;
const file = filePickerInput.files[0];
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target.result;
const id = Spicetify.URI.from(filePickerInput.uri).id;
try {
localStorage.setItem("dribbblish:folder-image:" + id, result);
} catch {
Spicetify.showNotification("File too large");
}
DribbblishShared.loadPlaylistImage?.call();
};
reader.readAsDataURL(file);
};
new Spicetify.ContextMenu.Item(
"Remove folder image",
([uri]) => {
const id = Spicetify.URI.from(uri).id;
localStorage.removeItem("dribbblish:folder-image:" + id);
DribbblishShared.loadPlaylistImage?.call();
},
([uri]) => Spicetify.URI.isFolder(uri),
"x"
).register();
new Spicetify.ContextMenu.Item(
"Choose folder image",
([uri]) => {
filePickerInput.uri = uri;
filePickerForm.reset();
filePickerInput.click();
},
([uri]) => Spicetify.URI.isFolder(uri),
"edit"
).register();
})();

672
src/js/main.js Normal file
View file

@ -0,0 +1,672 @@
import * as Vibrant from "node-vibrant";
import chroma from "chroma-js";
import $ from "jquery";
import ConfigMenu from "./ConfigMenu";
class _DribbblishShared {
constructor() {
this.config = new ConfigMenu();
}
}
const DribbblishShared = new _DribbblishShared();
DribbblishShared.config.register({
type: "checkbox",
key: "rightBigCover",
name: "Right expanded cover",
description: "Have the expanded cover Image on the right instead of on the left",
defaultValue: true,
onChange: (val) => $("html").toggleClass("right-expanded-cover", val)
});
DribbblishShared.config.register({
type: "checkbox",
key: "roundSidebarIcons",
name: "Round Sidebar Icons",
description: "If the Sidebar Icons should be round instead of square",
defaultValue: false,
onChange: (val) => $("html").css("--sidebar-icons-border-radius", val ? "50%" : "var(--image-radius)")
});
DribbblishShared.config.register({
area: "Animations & Transitions",
type: "checkbox",
key: "sidebarHoverAnimation",
name: "Sidebar Hover Animation",
description: "If the Sidebar Icons should have an animated background on hover",
defaultValue: true,
onChange: (val) => $("html").css("--sidebar-icons-hover-animation", val ? "1" : "0")
});
waitForElement(["#main"], () => {
DribbblishShared.config.register({
type: "select",
data: { none: "None", "none-padding": "None (With Top Padding)", solid: "Solid", transparent: "Transparent" },
key: "winTopBar",
name: "Windows Top Bar",
description: "Have different top Bars (or none at all)",
defaultValue: "none",
onChange: (val) => $("#main").attr("top-bar", val)
});
DribbblishShared.config.register({
type: "select",
data: { dribbblish: "Dribbblish", spotify: "Spotify" },
key: "playerControlsStyle",
name: "Player Controls Style",
description: "Style of the Player Controls. Selecting Spotify basically changes Play / Pause back to the center",
defaultValue: "dribbblish",
onChange: (val) => $("#main").attr("player-controls", val)
});
DribbblishShared.config.register({
area: "Ads",
type: "checkbox",
key: "hideAds",
name: "Hide Ads",
description: `Hide ads / premium features (see: <a href="https://github.com/Daksh777/SpotifyNoPremium">SpotifyNoPremium</a>)`,
defaultValue: false,
onChange: (val) => $("#main").attr("hide-ads", val)
});
});
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);
}
}
waitForElement([`.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"]`, `.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"] li`], ([root, firstItem]) => {
const listElem = firstItem.parentElement;
root.classList.add("dribs-playlist-list");
/** Replace Playlist name with their pictures */
function loadPlaylistImage() {
for (const item of listElem.children) {
let link = item.querySelector("a");
if (!link) continue;
let [_, app, uid] = link.pathname.split("/");
let uri;
if (app === "playlist") {
uri = Spicetify.URI.playlistV2URI(uid);
} else if (app === "folder") {
const base64 = localStorage.getItem("dribbblish:folder-image:" + uid);
let img = link.querySelector("img");
if (!img) {
img = document.createElement("img");
img.classList.add("playlist-picture");
link.prepend(img);
}
img.src = base64 || "/images/tracklist-row-song-fallback.svg";
continue;
}
Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri.toURI()}/metadata`, { policy: { picture: true } }).then((res) => {
const meta = res.metadata;
let img = link.querySelector("img");
if (!img) {
img = document.createElement("img");
img.classList.add("playlist-picture");
link.prepend(img);
}
img.src = meta.picture || "/images/tracklist-row-song-fallback.svg";
});
}
}
DribbblishShared.loadPlaylistImage = loadPlaylistImage;
loadPlaylistImage();
new MutationObserver(loadPlaylistImage).observe(listElem, { childList: true });
});
waitForElement([".main-rootlist-rootlist", ".main-rootlist-wrapper > :nth-child(2) > :first-child", "#spicetify-show-list"], ([rootlist]) => {
function checkSidebarPlaylistScroll() {
const topDist = rootlist.getBoundingClientRect().top - document.querySelector("#spicetify-show-list:not(:empty), .main-rootlist-wrapper > :nth-child(2) > :first-child").getBoundingClientRect().top;
const bottomDist = document.querySelector(".main-rootlist-wrapper > :nth-child(2) > :last-child").getBoundingClientRect().bottom - rootlist.getBoundingClientRect().bottom;
rootlist.classList.remove("no-top-shadow", "no-bottom-shadow");
if (topDist < 10) rootlist.classList.add("no-top-shadow");
if (bottomDist < 10) rootlist.classList.add("no-bottom-shadow");
}
checkSidebarPlaylistScroll();
// Use Interval because scrolling takes a while and getBoundingClientRect() gets position at the moment of calling, so the interval keeps calling for 1s
let c = 0;
let interval;
rootlist.addEventListener("wheel", () => {
checkSidebarPlaylistScroll();
c = 0;
if (interval == null)
interval = setInterval(() => {
if (c > 20) {
clearInterval(interval);
interval = null;
return;
}
checkSidebarPlaylistScroll();
c++;
}, 50);
});
});
waitForElement([".Root__main-view"], ([mainView]) => {
const shadow = document.createElement("div");
shadow.id = "dribbblish-back-shadow";
mainView.prepend(shadow);
});
waitForElement([".Root__nav-bar .LayoutResizer__input, .Root__nav-bar .LayoutResizer__resize-bar input"], ([resizer]) => {
const observer = new MutationObserver(updateVariable);
observer.observe(resizer, { attributes: true, attributeFilter: ["value"] });
function updateVariable() {
let value = resizer.value;
if (value < 121) value = 72;
$("html").toggleClass("sidebar-hide-text", value < 121);
$("html").css("--sidebar-width", `${value}px`);
}
updateVariable();
});
waitForElement([".Root__main-view .os-resize-observer-host"], ([resizeHost]) => {
const observer = new ResizeObserver(updateVariable);
observer.observe(resizeHost);
function updateVariable([event]) {
$("html").css("--main-view-width", event.contentRect.width + "px");
$("html").css("--main-view-height", event.contentRect.height + "px");
$("html").toggleClass("minimal-player", event.contentRect.width < 700);
$("html").toggleClass("extra-minimal-player", event.contentRect.width < 550);
}
});
(function Dribbblish() {
const progBar = document.querySelector(".playback-bar");
const root = document.querySelector(".Root");
if (!Spicetify.Player.origin || !progBar || !root) {
setTimeout(Dribbblish, 300);
return;
}
const progKnob = progBar.querySelector(".progress-bar__slider");
const tooltip = document.createElement("div");
tooltip.className = "prog-tooltip";
progKnob.append(tooltip);
function updateProgTime(timeOverride) {
const newText = Spicetify.Player.formatTime(timeOverride || Spicetify.Player.getProgress()) + " / " + Spicetify.Player.formatTime(Spicetify.Player.getDuration());
// To reduce DOM Updates when the Song is Paused
if (tooltip.innerText != newText) tooltip.innerText = newText;
}
const knobPosObserver = new MutationObserver((muts) => {
const progressPercentage = Number($(".progress-bar").css("--progress-bar-transform").replace("%", "")) / 100;
updateProgTime(Spicetify.Player.getDuration() * progressPercentage);
});
knobPosObserver.observe(document.querySelector(".progress-bar"), {
attributes: true,
attributeFilter: ["style"]
});
Spicetify.Player.addEventListener("songchange", () => updateProgTime());
updateProgTime();
Spicetify.CosmosAsync.sub("sp://connect/v1", (state) => {
const isExternal = state.devices.some((a) => a.is_active);
if (isExternal) {
root.classList.add("is-connectBarVisible");
} else {
root.classList.remove("is-connectBarVisible");
}
});
const filePickerForm = document.createElement("form");
filePickerForm.setAttribute("aria-hidden", true);
filePickerForm.innerHTML = '<input type="file" class="hidden-visually" />';
document.body.appendChild(filePickerForm);
/** @type {HTMLInputElement} */
const filePickerInput = filePickerForm.childNodes[0];
filePickerInput.accept = ["image/jpeg", "image/apng", "image/avif", "image/gif", "image/png", "image/svg+xml", "image/webp"].join(",");
filePickerInput.onchange = () => {
if (!filePickerInput.files.length) return;
const file = filePickerInput.files[0];
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target.result;
const id = Spicetify.URI.from(filePickerInput.uri).id;
try {
localStorage.setItem("dribbblish:folder-image:" + id, result);
} catch {
Spicetify.showNotification("File too large");
}
DribbblishShared.loadPlaylistImage?.call();
};
reader.readAsDataURL(file);
};
new Spicetify.ContextMenu.Item(
"Remove folder image",
([uri]) => {
const id = Spicetify.URI.from(uri).id;
localStorage.removeItem("dribbblish:folder-image:" + id);
DribbblishShared.loadPlaylistImage?.call();
},
([uri]) => Spicetify.URI.isFolder(uri),
"x"
).register();
new Spicetify.ContextMenu.Item(
"Choose folder image",
([uri]) => {
filePickerInput.uri = uri;
filePickerForm.reset();
filePickerInput.click();
},
([uri]) => Spicetify.URI.isFolder(uri),
"edit"
).register();
})();
/* Config settings */
DribbblishShared.config.register({
area: "Animations & Transitions",
type: "slider",
key: "fadeDuration",
name: "Color Fade Duration",
description: "Select the duration of the color fading transition",
defaultValue: 0.5,
data: {
min: 0,
max: 10,
step: 0.1,
suffix: "s"
},
onChange: (val) => $("html").css("--song-transition-speed", `${val}s`)
});
// waitForElement because Spicetify is not initialized at startup
waitForElement(["#main"], () => {
DribbblishShared.config.register({
area: { name: "About", order: 999 },
type: "button",
key: "aboutDribbblish",
name: "Info",
description: `
OS: ${capitalizeFirstLetter(Spicetify.Platform.PlatformData.os_name)} v${Spicetify.Platform.PlatformData.os_version}
Spotify: v${Spicetify.Platform.PlatformData.event_sender_context_information?.client_version_string ?? Spicetify.Platform.PlatformData.client_version_triple}
Dribbblish: v${process.env.DRIBBBLISH_VERSION}
`,
data: "Copy",
onChange: (val) => {
copyToClipboard(DribbblishShared.config.getOptions("aboutDribbblish").description);
Spicetify.showNotification("Copied Versions");
}
});
});
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function copyToClipboard(text) {
var input = document.createElement("textarea");
input.style.display = "fixed";
input.innerHTML = text;
document.body.appendChild(input);
input.select();
var result = document.execCommand("copy");
document.body.removeChild(input);
return result;
}
/* js */
function getAlbumInfo(uri) {
return Spicetify.CosmosAsync.get(`hm://album/v1/album-app/album/${uri}/desktop`);
}
function isLight(hex) {
var [r, g, b] = chroma(hex).rgb().map(Number);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128;
}
// From: https://stackoverflow.com/a/13763063/12126879
function getImageLightness(img) {
var colorSum = 0;
var canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
var r, g, b, avg;
for (var x = 0, len = data.length; x < len; x += 4) {
r = data[x];
g = data[x + 1];
b = data[x + 2];
avg = Math.floor((r + g + b) / 3);
colorSum += avg;
}
var brightness = Math.floor(colorSum / (img.width * img.height));
return brightness;
}
// parse to hex beacuse "--spice-sidebar" is `rgb()`
let textColor = chroma($("html").css("--spice-text")).hex();
let textColorBg = chroma($("html").css("--spice-main")).hex();
let sidebarColor = chroma($("html").css("--spice-sidebar")).hex();
function setRootColor(name, colHex) {
$("html").css(`--spice-${name}`, colHex);
$("html").css(`--spice-rgb-${name}`, chroma(colHex).rgb().join(","));
}
function toggleDark(setDark) {
if (setDark === undefined) setDark = isLight(textColorBg);
$("html").css("--is_light", setDark ? 0 : 1);
textColorBg = setDark ? "#0A0A0A" : "#FAFAFA";
setRootColor("main", textColorBg);
setRootColor("player", textColorBg);
setRootColor("card", setDark ? "#040404" : "#ECECEC");
setRootColor("subtext", setDark ? "#EAEAEA" : "#3D3D3D");
setRootColor("notification", setDark ? "#303030" : "#DDDDDD");
updateColors(textColor, sidebarColor, false);
}
function checkDarkLightMode(colors) {
const theme = DribbblishShared.config.get("theme");
if (theme == "time") {
const start = 60 * parseInt(DribbblishShared.config.get("darkModeOnTime").split(":")[0]) + parseInt(DribbblishShared.config.get("darkModeOnTime").split(":")[1]);
const end = 60 * parseInt(DribbblishShared.config.get("darkModeOffTime").split(":")[0]) + parseInt(DribbblishShared.config.get("darkModeOffTime").split(":")[1]);
const now = new Date();
const time = 60 * now.getHours() + now.getMinutes();
let dark;
if (end < start) dark = start <= time || time < end;
else dark = start <= time && time < end;
toggleDark(dark);
} else if (theme == "color") {
if (colors && colors.length > 0) toggleDark(isLight(colors[0]));
}
}
// Run every Minute to check time and set dark / light mode
setInterval(checkDarkLightMode, 60000);
DribbblishShared.config.register({
area: "Theme",
type: "checkbox",
key: "dynamicColors",
name: "Dynamic",
description: "If the Theme's Color should be extracted from Albumart",
defaultValue: true,
onChange: (val) => updateColors(),
showChildren: (val) => !val,
children: [
{
type: "color",
key: "colorOverride",
name: "Color",
description: "The Color of the Theme",
defaultValue: "#1ed760",
fireInitialChange: false,
onChange: (val) => updateColors()
}
]
});
DribbblishShared.config.register({
area: "Theme",
type: "select",
data: { dark: "Dark", light: "Light", time: "Based on Time", color: "Based on Color" },
key: "theme",
name: "Theme",
description: "Select Dark / Bright mode",
defaultValue: "dark",
showChildren: (val) => {
if (val == 2) return ["darkModeOnTime", "darkModeOffTime"];
//if (val == 3) return [""];
return false;
},
onChange: (val) => {
switch (val) {
case "dark":
toggleDark(true);
break;
case "light":
toggleDark(false);
break;
case "time":
checkDarkLightMode();
break;
case "color":
checkDarkLightMode();
break;
}
},
children: [
{
type: "time",
key: "darkModeOnTime",
name: "Dark Mode On Time",
description: "Beginning of Dark mode time",
defaultValue: "20:00",
fireInitialChange: false,
onChange: checkDarkLightMode
},
{
type: "time",
key: "darkModeOffTime",
name: "Dark Mode Off Time",
description: "End of Dark mode time",
defaultValue: "06:00",
fireInitialChange: false,
onChange: checkDarkLightMode
}
]
});
var currentColor;
var currentSideColor;
function updateColors(textColHex, sideColHex, checkDarkMode = true) {
if (textColHex && sideColHex) {
currentColor = textColHex;
currentSideColor = sideColHex;
} else {
if (!(currentColor && currentSideColor)) return; // If `updateColors()` is called early these vars are undefined and would break
textColHex = currentColor;
sideColHex = currentSideColor;
}
if (!DribbblishShared.config.get("dynamicColors")) {
const col = DribbblishShared.config.get("colorOverride");
textColHex = col;
sideColHex = col;
}
let isLightBg = isLight(textColorBg);
if (isLightBg) textColHex = chroma(textColHex).darken(0.15).hex(); // vibrant color is always too bright for white bg mode
let darkColHex = chroma(textColHex)
.brighten(isLightBg ? 0.12 : -0.2)
.hex();
let darkerColHex = chroma(textColHex)
.brighten(isLightBg ? 0.3 : -0.4)
.hex();
let buttonBgColHex = chroma(textColHex)
.set("hsl.l", isLightBg ? 0.9 : 0.14)
.hex();
setRootColor("text", textColHex);
setRootColor("button", darkerColHex);
setRootColor("button-active", darkColHex);
setRootColor("selected-row", darkerColHex);
setRootColor("tab-active", buttonBgColHex);
setRootColor("button-disabled", buttonBgColHex);
setRootColor("sidebar", sideColHex);
if (checkDarkMode) checkDarkLightMode([textColHex, sideColHex]);
}
let nearArtistSpan;
let nearArtistSpanText = "";
let coverListenerInstalled = false;
async function songchange() {
try {
// warning popup
if (Spicetify.Platform.PlatformData.client_version_triple < "1.1.68") Spicetify.showNotification(`Your version of Spotify ${Spicetify.Platform.PlatformData.client_version_triple}) is un-supported`);
} catch (err) {
console.error(err);
}
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";
textColor = "#509bf5";
updateColors(textColor, textColor);
coverListenerInstalled = false;
}
if (!coverListenerInstalled) hookCoverChange(true);
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" });
let album_link = '<a title="' + Spicetify.Player.data.track.metadata.album_title + '" href="' + album_uri + '" data-uri="' + album_uri + '" data-interaction-target="album-name" class="tl-cell__content">' + Spicetify.Player.data.track.metadata.album_title + "</a>";
nearArtistSpanText = album_link + " • " + album_date;
} else if (Spicetify.Player.data.track.uri.includes("spotify:episode")) {
// podcast
bgImage = bgImage.replace("spotify:image:", "https://i.scdn.co/image/");
nearArtistSpanText = Spicetify.Player.data.track.metadata.album_title;
} else if (Spicetify.Player.data.track.metadata.is_local == "true") {
// local file
nearArtistSpanText = Spicetify.Player.data.track.metadata.album_title;
} else if (Spicetify.Player.data.track.provider == "ad") {
// ad
nearArtistSpanText = "advertisement";
coverListenerInstalled = false;
return;
} else {
// When clicking a song from the homepage, songChange is fired with half empty metadata
// todo: retry only once?
setTimeout(songchange, 200);
}
if (document.querySelector("#main-trackInfo-year") === null) {
waitForElement([".main-trackInfo-container"], (queries) => {
nearArtistSpan = document.createElement("div");
nearArtistSpan.id = "main-trackInfo-year";
nearArtistSpan.classList.add("main-trackInfo-artists", "ellipsis-one-line", "main-type-finale");
nearArtistSpan.innerHTML = nearArtistSpanText;
queries[0].append(nearArtistSpan);
});
} else {
nearArtistSpan.innerHTML = nearArtistSpanText;
}
$("html").css("--image-url", `url("${bgImage}")`);
}
Spicetify.Player.addEventListener("songchange", songchange);
async function pickCoverColor(img) {
if (!img.currentSrc.startsWith("spotify:")) return;
$("html").css("--image-brightness", getImageLightness(img) / 255);
var swatches = await new Promise((resolve, reject) => new Vibrant(img, 5).getPalette().then(resolve).catch(reject));
var lightCols = ["Vibrant", "DarkVibrant", "Muted", "LightVibrant"];
var darkCols = ["Vibrant", "LightVibrant", "Muted", "DarkVibrant"];
var mainCols = isLight(textColorBg) ? lightCols : darkCols;
textColor = "#509bf5";
for (var col in mainCols)
if (swatches[mainCols[col]]) {
textColor = swatches[mainCols[col]].getHex();
break;
}
sidebarColor = "#509bf5";
for (var col in lightCols)
if (swatches[lightCols[col]]) {
sidebarColor = swatches[lightCols[col]].getHex();
break;
}
updateColors(textColor, sidebarColor);
}
waitForElement([".main-nowPlayingBar-left"], (queries) => {
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.removedNodes.length > 0) coverListenerInstalled = false;
});
});
observer.observe(queries[0], { childList: true });
});
function hookCoverChange(pick) {
waitForElement([".cover-art-image"], (queries) => {
coverListenerInstalled = true;
var elem = queries.slice(-1)[0];
if (pick && elem.complete && elem.naturalHeight !== 0) pickCoverColor(elem);
elem.addEventListener("load", function () {
try {
pickCoverColor(elem);
} catch (error) {
console.error(error);
setTimeout(pickCoverColor, 300, elem);
}
});
});
}
hookCoverChange(false);
// Check latest release
waitForElement([".main-userWidget-box"], ([userWidget]) => {
fetch("https://api.github.com/repos/JulienMaille/dribbblish-dynamic-theme/releases/latest")
.then((response) => {
return response.json();
})
.then((data) => {
const upd = document.createElement("div");
upd.classList.add("ellipsis-one-line", "main-type-finale");
upd.setAttribute("title", `Changes: ${data.name}`);
upd.style.setProperty("color", "var(--spice-button-active)");
if (process.env.DRIBBBLISH_VERSION == "Dev") {
upd.innerText = "Dev version!";
} else if (data.tag_name > process.env.DRIBBBLISH_VERSION) {
upd.innerText = `Theme UPD v${data.tag_name} avail.`;
new Spicetify.Menu.Item("Update Dribbblish", false, () => window.open("https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest", "_blank")).register();
}
userWidget.append(upd);
userWidget.classList.add("update-avail");
})
.catch((err) => {
// Do something for an error here
console.error(err);
});
});
$("html").css("--warning_message", " ");

138
src/styles/ConfigMenu.scss Normal file
View file

@ -0,0 +1,138 @@
#dribbblish-config {
display: none;
z-index: 99999;
position: absolute;
inset: 0px;
align-items: center;
justify-content: center;
color: var(--spice-text);
&[active] {
display: flex;
}
.dribbblish-config-container {
z-index: 1;
position: relative;
width: clamp(500px, 50%, 650px);
background-color: rgba(var(--spice-rgb-main), 0.9);
backdrop-filter: blur(3px);
padding: 20px 15px;
border-radius: var(--main-corner-radius);
box-shadow: 0 0 10px 3px #0000003b;
display: flex;
gap: 5px;
flex-direction: column;
align-items: center;
justify-content: center;
.dribbblish-config-close {
position: absolute;
top: 15px;
right: 15px;
}
.dribbblish-config-areas {
display: flex;
width: 100%;
flex-direction: column;
gap: 16px;
max-height: 60vh;
overflow-y: auto;
padding: 0px 50px;
.dribbblish-config-area {
display: flex;
flex-direction: column;
gap: 16px;
&[collapsed] {
overflow: hidden;
min-height: 38px; //for some reason height alone isn't enough
height: 38px;
> h2 svg {
transform: rotate(270deg);
}
}
&:empty {
display: none;
}
.dribbblish-config-area-header {
position: relative;
text-align: center;
height: 38px;
svg {
position: absolute;
margin-left: 10px;
bottom: -2px;
color: var(--spice-text);
padding: 0px;
height: 100%;
stroke-width: 2px;
transform: rotate(90deg);
}
}
.dribbblish-config-area-items {
display: flex;
flex-direction: column;
gap: 16px;
.dribbblish-config-item {
position: relative;
width: 100%;
height: min-content;
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
gap: 5px 10px;
grid-template-areas: "header input" "description input";
&[parent] {
padding-left: 16px;
}
&[hidden="true"] {
display: none;
}
> {
.x-settings-title {
grid-area: header;
margin: 0px;
height: min-content;
position: relative;
bottom: 0px;
&.no-desc {
bottom: -10px;
}
}
.main-type-mesto {
grid-area: description;
height: min-content;
color: var(--spice-subtext);
}
.x-settings-secondColumn {
grid-area: input;
}
}
}
}
}
}
}
.dribbblish-config-backdrop {
position: absolute;
content: "";
inset: 0px;
backdrop-filter: blur(3px) brightness(60%);
}
}

114
src/styles/Inputs.scss Normal file
View file

@ -0,0 +1,114 @@
button.main-button-primary {
background-color: rgba(var(--spice-rgb-selected-row), 0.4) !important;
&:hover,
&:active {
background-color: rgba(var(--spice-rgb-selected-row), 0.6) !important;
}
span {
color: var(--spice-subtext) !important;
}
}
input,
textarea {
background-color: rgba(var(--spice-rgb-selected-row), 0.4) !important;
border-radius: 4px !important;
padding: 6px 10px 6px 48px;
color: var(--spice-text) !important;
&:hover,
&:active {
background-color: rgba(var(--spice-rgb-selected-row), 0.6) !important;
}
&[type="range"] {
-webkit-appearance: none;
background: transparent;
padding: 0px;
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
margin-top: -4px;
border-radius: 50%;
background-color: var(--spice-text);
&:hover,
&:active {
filter: brightness(80%);
}
}
&::after {
z-index: 9999;
content: attr(tooltip);
position: absolute;
min-width: 50px;
top: -10px;
left: 50%;
transform: translateX(calc(-50%));
padding: 0 5px;
border-radius: 4px;
text-align: center;
color: var(--spice-sidebar-text);
background-color: var(--spice-button);
transition: opacity 0.25s ease;
opacity: 0;
}
&:hover::after,
&:active::after {
opacity: 1;
}
&:focus {
outline: none;
}
&::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
background-color: rgba(var(--spice-rgb-text), 0.2);
border-radius: 50vw;
}
}
&[type="number"],
&[type="text"],
&[type="time"] {
height: 32px;
border: none;
border-radius: 4px !important;
padding: 0px 10px;
color: var(--spice-subtext) !important;
}
&[type="time"] {
&::-webkit-calendar-picker-indicator {
filter: invert(calc(1 - var(--is_light)));
}
}
&[type="color"] {
position: relative;
padding: 0px;
border: none;
&::before {
z-index: -1;
content: "";
position: absolute;
inset: -5px;
border-radius: 4px;
background-color: rgba(var(--spice-rgb-selected-row), 0.4);
}
&:hover::before,
&:active::before {
background-color: rgba(var(--spice-rgb-selected-row), 0.6);
}
}
}

17
src/styles/NoAds.scss Normal file
View file

@ -0,0 +1,17 @@
#main[hide-ads="true"] {
/* Remove upgrade button*/
.main-topBar-UpgradeButton {
display: none;
}
/* Remove upgrade to premium button in user menu */
.main-contextMenu-menuItemButton[href="https://www.spotify.com/premium/"]
{
display: none;
}
/* Remove ad placeholder in main screen */
.main-leaderboardComponent-container {
display: none;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
spicetify config current_theme " " extensions dribbblish.js- extensions dribbblish-dynamic.js- extensions Vibrant.min.js-
spicetify config current_theme " " extensions dribbblish-dynamic.js-
$spicePath = spicetify -c | Split-Path
$configFile = Get-Content "$spicePath\config-xpui.ini"

56
webpack.config.js Normal file
View file

@ -0,0 +1,56 @@
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const path = require("path");
/** @type {import('webpack').Configuration} */
module.exports = {
entry: [path.resolve(__dirname, "./src/js/main.js"), path.resolve(__dirname, "./src/styles/main.scss")],
output: {
path: path.resolve(__dirname, "dist"),
filename: "dribbblish-dynamic.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: []
},
{
test: /\.scss$/,
exclude: /node_modules/,
type: "asset/resource",
generator: {
filename: "user.css"
},
use: [
{
loader: "sass-loader",
options: {
sourceMap: true
}
}
]
}
]
},
devtool: "inline-source-map",
plugins: [
new CleanWebpackPlugin({
protectWebpackAssets: false,
cleanAfterEveryBuildPatterns: ["*.LICENSE.txt"]
}),
new webpack.DefinePlugin({
"process.env.DRIBBBLISH_VERSION": JSON.stringify(process.env.DRIBBBLISH_VERSION || "Dev")
}),
new CopyPlugin({
patterns: [{ from: "src/assets", to: "assets" }, { from: "src/color.ini" }]
})
],
optimization: {
minimize: true,
minimizer: [`...`, new CssMinimizerPlugin()]
}
};