Compare commits

...

219 commits
3.1.0 ... main

Author SHA1 Message Date
Julien
77182042dd
Merge pull request #209 from JulienMaille/dependabot/npm_and_yarn/markdown-it-12.3.2
Bump markdown-it from 12.2.0 to 12.3.2
2022-04-16 13:17:20 +02:00
dependabot[bot]
7492c86124
Bump markdown-it from 12.2.0 to 12.3.2
Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 12.2.0 to 12.3.2.
- [Release notes](https://github.com/markdown-it/markdown-it/releases)
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/12.2.0...12.3.2)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 17:06:59 +00:00
Julien
55693078f6
Merge pull request #214 from JulienMaille/dependabot/npm_and_yarn/moment-2.29.2
Bump moment from 2.29.1 to 2.29.2
2022-04-09 19:06:17 +02:00
dependabot[bot]
2c8dea7f5d
Bump moment from 2.29.1 to 2.29.2
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 13:32:51 +00:00
Julien Maille
6d23932d0b FIX #190 Album covers not being rounded edge in latest played 2022-04-04 23:55:59 +02:00
Julien Maille
d7e355bf46 FIX: wrong font size for genre element 2022-04-04 23:55:59 +02:00
Julien
c9ec3971ad
Merge pull request #207 from JulienMaille/dependabot/npm_and_yarn/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6
2022-04-04 23:53:07 +02:00
dependabot[bot]
bb7017960b
Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-31 23:02:12 +00:00
Julien Maille
1064087b48 Fix color of playlist descriptions FIX #198 2022-03-27 15:32:12 +02:00
Julien Maille
3edfc3fa6b Hightlighting current playing song FIX #189 2022-03-27 15:28:43 +02:00
Julien Maille
239033ad5e Try to fix genre display when playing a podcast #200 2022-03-26 14:48:12 +01:00
Julien Maille
145cfe9b53 Replace deprecated hermes protocol FIX #199 2022-03-26 14:19:13 +01:00
Julien
2e55b23b44
Create stale.yml 2022-03-15 10:18:55 +01:00
Julien Maille
8c5a45c64a temporary remove marketplace integration 2022-03-13 11:12:53 +01:00
Julien Maille
8d9cdf3df5 FIX progress-bar hover color 2022-03-04 08:24:33 +01:00
Julien Maille
9f5a988eca typo 2022-02-28 20:52:36 +01:00
Julien Maille
167e253a86 ok now button colors should look fine 2022-02-27 22:28:13 +01:00
Erik
ba5846bc41
Merge pull request #180 from JulienMaille/fix-blue
fix color shifting to blue for a second (#179)
2022-02-27 21:27:27 +01:00
Julien Maille
d687a13cb7 revert commit, button color are broken without this 2022-02-27 21:22:48 +01:00
Send_Nukez
33134709ed fix color shifting to blue for a second (#179) 2022-02-27 21:13:30 +01:00
Julien Maille
13e1f64054 IMP: installation script 2022-02-27 20:30:29 +01:00
Julien Maille
827c9f3ba0 proper fix for #174 #169 and recent css changes 2022-02-27 20:10:00 +01:00
Julien
f13a3c1ff9
Merge pull request #178 from f1shy-dev/main
Minor fixes
2022-02-27 20:01:29 +01:00
f1shy-dev
14da00bf74 remove the control button triangle 2022-02-27 18:36:21 +00:00
f1shy-dev
23fe7deb15 add a comment to show the issue ID 2022-02-27 18:02:34 +00:00
f1shy-dev
a146134b1d fixes 2022-02-27 17:59:05 +00:00
Send_Nukez
4c134a94a0 update changelog 2022-02-03 19:56:44 +01:00
Erik
14902a060f Merge pull request #165 from Shinyhero36/playback-genre
Add option to display the genre of the current playback
2022-02-03 16:40:22 +00:00
Gérald LEBAN
c45c5faad0 Change description and set default to false 2022-02-03 17:01:52 +01:00
Send_Nukez
162616c2cb hopefully fix actions 2022-02-03 16:18:05 +01:00
Send_Nukez
301a972a53 add @babel/core as devDependency. resolves #164 2022-02-03 15:52:25 +01:00
Gérald LEBAN
290df59e02 Add option to display the genre of the current playback 2022-02-03 13:10:56 +01:00
Send_Nukez
3e2cde1e53 Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2022-01-23 13:59:58 +01:00
Julien Maille
eb9a7b1939 FIX: clean uninstall of the theme 2022-01-23 11:12:04 +01:00
Send_Nukez
7d3341b7a7 Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2022-01-23 11:07:24 +01:00
Send_Nukez
d78ae80ef1 make sass functions available in js & color playlist visualizer 2022-01-23 10:03:59 +00:00
Send_Nukez
316e7581cf make sass functions available in js & color playlist visualizer 2022-01-23 11:03:40 +01:00
Send_Nukez
36fe1f5a2f fix #147 2022-01-23 10:44:04 +01:00
Julien Maille
273a5b3e7e FIX: Wrong volume slider color on hover #149 2022-01-22 11:52:46 +01:00
Julien Maille
f9c30d230a FIX: automatic path detection when patchin spotify.exe #155 2022-01-16 16:42:25 +01:00
Julien
bc200bfef6
FIX #150 transparent title bar 2021-12-31 23:38:49 +01:00
Send_Nukez
13186767e8 update loading.svg 2021-12-24 21:54:24 +01:00
Send_Nukez
d69c825555 update actions to use node 15 2021-12-24 21:51:24 +01:00
Send_Nukez
56b109cc01 refactor stuff and change notification styles 2021-12-24 20:41:17 +00:00
Send_Nukez
decfe17aa2 add package-lock.json 2021-12-23 23:32:14 +01:00
Send_Nukez
b55a82bf99 fix prettier action 2021-12-23 23:31:07 +01:00
Send_Nukez
c052260a8b make automatic bug reports use <details> 2021-12-20 23:04:46 +01:00
Send_Nukez
ee98ffd134 use loadsh.defaultsDeep to deep merge options 2021-12-19 23:18:06 +01:00
Send_Nukez
305d70fb6d fix user image context menu 2021-12-19 22:15:31 +01:00
Send_Nukez
e50b8b4bb3 remove "spotify" as color selection algorithm 2021-12-18 23:23:34 +01:00
Send_Nukez
ca0d24fd11 refactor loader & icons a bit 2021-12-18 23:20:42 +01:00
Send_Nukez
fa29a12d00 throw an error if the ready event is not fired after 50 tries 2021-12-18 22:59:12 +01:00
Send_Nukez
0ecd8d6166 change custom app tab styles 2021-12-18 20:26:04 +01:00
Send_Nukez
c5fec9cbd1 fix custom search input not being focussed after clicking 2021-12-18 17:03:24 +01:00
Send_Nukez
11eae40857 added ability to hide premium features in addition to ads 2021-12-16 12:19:27 +01:00
Send_Nukez
604ddfbf97 add embedWidgetGenerator modals to custom modal styles 2021-12-15 10:03:17 +01:00
Send_Nukez
1b45d68568 Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2021-12-13 21:56:43 +01:00
Julien Maille
2bfa6e55db IMP: patch-dark-mode.ps1 2021-12-13 16:57:12 +01:00
Julien
48c475ee9a
FIX #142
thank you @alixti
2021-12-13 16:43:50 +01:00
Send_Nukez
8517327ee6 Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2021-12-10 20:45:17 +01:00
Send_Nukez
e2e6214159 add stable release to marketplace 2021-12-10 19:44:21 +00:00
Send_Nukez
7ac95c23a9 add stable release to marketplace 2021-12-10 20:43:59 +01:00
github-actions
048d007430 Release v4.1.1 2021-12-10 19:40:43 +00:00
Send_Nukez
0e50a92431 refactor icons a bit 2021-12-10 20:11:46 +01:00
Send_Nukez
4dd2d42ca4 changed default sidebar color to be darker & improve loader styles 2021-12-10 20:05:07 +01:00
Send_Nukez
ead9615ef3 Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2021-12-10 19:23:29 +01:00
Send_Nukez
cd325a3f05 fix #137 2021-12-10 18:20:31 +00:00
Send_Nukez
6c84a848fc fix #137 2021-12-10 19:20:07 +01:00
github-actions
b3e108f2bf Release v4.1.0 2021-12-10 15:41:22 +00:00
Julien Maille
61cbee1c1d improved settings description 2021-12-06 20:34:30 +01:00
Send_Nukez
8edba57dd5 make System the default theme 2021-12-04 15:49:51 +01:00
Send_Nukez
55e6ef0ca2 add ability to get icons as raw svg string without type annd stuff 2021-12-04 15:24:01 +01:00
Send_Nukez
25b5b771dd Channge font of "Home Shelf" titles 2021-12-04 15:21:25 +01:00
Send_Nukez
9e7349712b add ability to disable config inputs & make disabled inputs be styled correctly 2021-12-04 15:20:53 +01:00
Send_Nukez
0e946cf05a Reword and refactor Theme > Theme System 2021-12-04 15:17:59 +01:00
Julien
f69afbb882
Update README.md 2021-12-04 14:40:51 +01:00
Julien Maille
467a971d31 FIX uninstall.sh 2021-12-04 14:29:02 +01:00
Julien Maille
55b714dbda added patch-dark-mode.ps1 2021-12-04 14:13:46 +01:00
Julien Maille
34bd5fe4f8 IMP: description for 'Based on System Theme' option 2021-12-02 20:43:29 +01:00
Julien
76fda08a71 Merge pull request #128 from evanbrierton/main
Implement functionality to use system theme for dark/light theme.
2021-12-02 19:30:29 +00:00
Send_Nukez
651896418c refactor / improve settings 2021-12-02 16:02:20 +01:00
Send_Nukez
e6ef0953ea add ability to search settings 2021-12-02 14:01:56 +01:00
Send_Nukez
8968202a3e move sass invert into Util 2021-12-01 06:15:33 +01:00
Send_Nukez
8747efdd7b reword some descriptions 2021-12-01 06:10:25 +01:00
Send_Nukez
3278f1fbe0 fix alignment of color inputs 2021-12-01 06:07:59 +01:00
Send_Nukez
0794a51b56 (hopefully) fix actions breaking on PRs 2021-12-01 02:55:59 +01:00
Evan Brierton
200b112fe1
Add fix to switch to system theme when option is selected 2021-11-30 12:00:41 +00:00
Julien
193953dc91
Merge pull request #129 from JulienMaille/bw-themes
WIP: add 3 b&w themes
2021-11-30 09:39:19 +01:00
Send_Nukez
1c4ba1a29d make bgTheme a child of theme and reword stuff a little 2021-11-30 02:57:02 +01:00
Julien
9ed25390c0
Update main.js
typo
2021-11-29 22:46:09 +01:00
Julien Maille
164058b875 WIP: add 3 b&w themes 2021-11-29 21:44:32 +01:00
Evan Brierton
eb87c41128
Actually read theme from config 2021-11-29 20:31:40 +00:00
Evan Brierton
5d5d1bc3c1
Use event listener for match system theme 2021-11-29 20:24:55 +00:00
Evan Brierton
836c13abec
Revert setInterval to once per minute for checkDarkLightMode 2021-11-29 20:22:08 +00:00
Evan Brierton
7e96398958
Delete .DS_Store 2021-11-29 13:32:16 +00:00
Evan Brierton
1a61984656
Increase frequency of checkDarkLightMode interval to 1s 2021-11-29 13:28:16 +00:00
Evan Brierton
7e59834be4
Implement system theme check in checkDarkLightMode 2021-11-29 13:28:01 +00:00
Evan Brierton
80b4d68835
Add System Theme option to config register 2021-11-29 13:27:32 +00:00
Send_Nukez
3629e883a5 add option to insert custom CSS 2021-11-29 07:00:44 +01:00
Send_Nukez
7f559d2a2c only show js not installed message after 3s 2021-11-29 04:37:46 +01:00
Send_Nukez
27cd9a8a0e automatically inject bug_report.md into js 2021-11-29 04:19:49 +01:00
Send_Nukez
b2cfe07578 overhaul modals and casting to device ui 2021-11-28 04:57:07 +01:00
Send_Nukez
e18320acb3 prevent js from being executed multiple times 2021-11-28 01:59:53 +01:00
Send_Nukez
2d30ff934e update readme to use absolute urls 2021-11-28 01:31:14 +01:00
Send_Nukez
cd24ce8f06 update manifest readme url 2021-11-28 00:59:47 +01:00
Erik
a00193acde
Merge pull request #125 from CharlieS1103/patch-1
Update manifest preview gif
2021-11-28 00:58:09 +01:00
CharlieS1103
cf9f6e154b
Update manifest preview gif
Should work with new changes to allow links, currently only did it for previews since that seems to be the only one with a demand for it.
2021-11-27 12:34:13 -05:00
Send_Nukez
6ac36c80a3 refactor Icons.js a bit 2021-11-27 13:23:15 +01:00
Send_Nukez
b60682cac5 change actions to include commit hash in version file 2021-11-27 13:18:09 +01:00
Send_Nukez
fdfe690b79 show beta build info & add spicetify icon 2021-11-27 13:14:03 +01:00
Send_Nukez
83546ad90c fix manifest 2021-11-27 12:57:13 +01:00
Send_Nukez
098a112819 change default info icon size 2021-11-27 12:45:53 +01:00
Send_Nukez
7c2fbea06f update / refactor styles a bit 2021-11-27 10:11:55 +01:00
Send_Nukez
9994713e55 only display option to reset setting if it was changed 2021-11-27 10:11:05 +01:00
Send_Nukez
d206317f45 remove redundant info from automatic but reports 2021-11-27 10:06:16 +01:00
Send_Nukez
2e1b7a05b5 update bug / feature report templates to use comments 2021-11-27 10:00:31 +01:00
Send_Nukez
8e702a49e1 rename release branches 2021-11-27 09:49:49 +01:00
Send_Nukez
5d537336a9 use material-icons 2021-11-27 08:22:27 +00:00
Send_Nukez
bcafa8b5a4 improve custom search bar handling 2021-11-27 00:44:15 +01:00
Send_Nukez
6cc4b7d492 remove redundant "Theme" in manifest title 2021-11-27 00:42:58 +01:00
Julien Maille
af5af92b62 test manifest for marketplace 2021-11-26 22:56:46 +01:00
Send_Nukez
5e0236dff5 improve offline icon handling 2021-11-26 04:04:30 +01:00
Send_Nukez
ea1ba797f6 make default popup styles fit inline with custom ones 2021-11-26 04:01:37 +01:00
Send_Nukez
a5b92ed62c also show spicetify update & fix Info.js 2021-11-25 13:16:48 +01:00
Send_Nukez
3798bc2723 update Loader.scss 2021-11-25 12:36:49 +01:00
Send_Nukez
61db2917f8 refactor icons to be imported as function 2021-11-25 12:21:25 +01:00
Send_Nukez
89742ae74a add hover & active state to config area headers 2021-11-25 09:59:36 +01:00
Send_Nukez
4328a81a1c add tooltip to playlist / folder images & refactor 2021-11-25 03:57:01 +01:00
Send_Nukez
219c16872c remove markdown-it-bracketed-spans 2021-11-24 23:03:49 +01:00
Send_Nukez
1296e13c73 tweak context menus a bit 2021-11-24 10:00:21 +01:00
Send_Nukez
c471531e71 update manifest 2021-11-23 21:48:13 +01:00
Send_Nukez
e4110f8df3 add maifest for spicetify-marketplace 2021-11-23 21:31:37 +01:00
Send_Nukez
38d70581ac prepare for spicetify-marketplace 2021-11-23 21:22:21 +01:00
Send_Nukez
3f308a2c9e add ability to disable the progress transition to improve performance 2021-11-23 20:39:10 +01:00
Send_Nukez
328cf2a59a re-add old search bar 2021-11-23 07:28:18 +01:00
Send_Nukez
4b251dfa8d add ability for loading screen to be disabled 2021-11-22 02:13:35 +01:00
Send_Nukez
f0a562f70a add basic event functionality and a loader at startup 2021-11-21 22:54:41 +00:00
Send_Nukez
83b2801a36 fix config item childrens order being wrong 2021-11-21 23:47:16 +01:00
Send_Nukez
675ff2d94d inject fonts into user.css as base64 url 2021-11-21 22:30:46 +01:00
Send_Nukez
8264eb33dd change default folder & playlist image and refactor 2021-11-20 22:02:03 +00:00
Send_Nukez
fe0503e9f8 fix #114 2021-11-20 18:18:25 +01:00
Julien
9afccf31c3
Update README.md 2021-11-18 22:12:10 +01:00
Julien Maille
980ed8d747 FIX: missing image for playlist without cover 2021-11-18 20:34:30 +01:00
Send_Nukez
71ce39abdb fix #116 2021-11-18 17:49:43 +01:00
Send_Nukez
c2d7f851b1 move Hide Ads into main settings 2021-11-18 02:45:05 +01:00
Send_Nukez
61db261115 refactor styles a bit 2021-11-18 02:44:15 +01:00
Send_Nukez
18f0d454be refactor update checking 2021-11-17 23:30:00 +01:00
Send_Nukez
f45f570730 fix checking for updates every 10m 2021-11-17 23:25:05 +01:00
github-actions
a6c1a89e5c Release v4.0.0 2021-11-17 20:42:29 +00:00
Send_Nukez
8ba60b477a update changelog 2021-11-17 21:35:11 +01:00
Send_Nukez
4538c1bb4b update Color Selection Algorithm description 2021-11-17 21:28:37 +01:00
Erik
f6aa2a5c7d
Merge pull request #113 from JulienMaille/colorthief
Colorthief
2021-11-17 21:26:51 +01:00
Send_Nukez
3448e7cffc add Request Feature button to about 2021-11-17 21:20:01 +01:00
Julien Maille
2b6853bc70 improve select dropdowns background color 2021-11-17 21:18:57 +01:00
Send_Nukez
58324a3ff2 update Color Selection Algorithm description 2021-11-17 21:09:01 +01:00
Send_Nukez
055510a7ad change markdownn muted styles to only affect text, not links and stuff 2021-11-17 21:05:25 +01:00
Send_Nukez
884ca0baea reorder markdown-it plugins 2021-11-17 21:04:39 +01:00
Send_Nukez
da404f4c7b Merge branch 'main' into colorthief 2021-11-17 20:22:58 +01:00
Send_Nukez
e1d393e2a0 Improve color settings UX and re-add vibrant as option 2021-11-17 20:22:53 +01:00
Send_Nukez
3088e8a71d add markdown-it-bracketed-spans 2021-11-17 20:18:21 +01:00
Send_Nukez
1b66ab044f remove uneccesary padding 2021-11-17 19:20:21 +01:00
Send_Nukez
3d96810b72 remove accidental log 2021-11-17 19:20:03 +01:00
Julien
3b248c465b
Merge pull request #112 from JulienMaille/JulienMaille-patch-1
set default theme mode to "Based on time"
2021-11-17 07:48:19 +01:00
Send_Nukez
a3cea3012e fix #111 2021-11-17 03:46:55 +01:00
Send_Nukez
784947e3a5 add option to select color extraction method 2021-11-17 03:40:09 +01:00
Julien Maille
3e41a07907 FIX unreadable buttons in modals 2021-11-17 00:07:41 +01:00
Julien
c48984b9ec
Fix z-index of left-side svgs 2021-11-16 22:48:55 +01:00
Julien
74140f1d42
set default theme mode to "Based on time" 2021-11-16 22:20:58 +01:00
Julien Maille
fd01cc02ce changelog 2021-11-16 22:15:46 +01:00
Send_Nukez
43452294db fix alignment of right expanded cover 2021-11-16 20:18:24 +01:00
Julien Maille
b610a0dd8b IMP: try to avoid black/white colors extracted from cover 2021-11-16 18:56:06 +01:00
Send_Nukez
1eddb9655e add ability for config validation to return strings as error to be displayed 2021-11-16 03:39:31 +01:00
Julien Maille
dfc2c79921 IMP: increase luminance of text 2021-11-15 21:22:18 +01:00
Send_Nukez
415efc5731 added option to have quick access to settings 2021-11-15 04:59:59 +01:00
Send_Nukez
f50ccaaf10 improve settings ui 2021-11-15 04:53:57 +01:00
Julien Maille
90662cd66d first shot (in the dark) 2021-11-14 23:07:03 +01:00
Send_Nukez
aaeb51a6ca rename DribbblishShared to Dribbblish and expose it to the window object to be used in other extensions 2021-11-14 00:25:44 +01:00
Send_Nukez
9d05365ffb refactor webpack config a bit and use dart-sass instead of node-sass 2021-11-14 00:24:05 +01:00
Send_Nukez
59daccdbf2 add ready state to info so it won't throw an error when registering items too early 2021-11-13 21:49:11 +01:00
Send_Nukez
a880871db0 add custom "offline" icon 2021-11-13 17:33:18 +01:00
Send_Nukez
215fbad04e make info text bolder 2021-11-13 17:25:20 +01:00
Send_Nukez
a77033801d don't use md for info and add more options to it 2021-11-13 17:24:37 +01:00
Send_Nukez
b1765a1e3c make the md prop private 2021-11-13 17:22:55 +01:00
Send_Nukez
284b3cc9f2 fix "invalid date" and show full date on hover 2021-11-12 22:25:21 +01:00
Julien
3b60f46002
Improve text color when sidebar color is light #106 2021-11-12 20:57:10 +01:00
Send_Nukez
5ebc888d42 don't minify css 2021-11-12 19:58:05 +01:00
Send_Nukez
b8d6d98883 update changelog 2021-11-12 19:40:54 +01:00
Send_Nukez
759c0e058f fix playing icon position being wrong when listening to a playlist that is inside a folder 2021-11-12 18:56:35 +01:00
Send_Nukez
d3b9577237 adjust padding of info items 2021-11-12 18:13:08 +01:00
Send_Nukez
0c318e1f21 Add Info class to show buttons / text next to the user icon (fixes #108) 2021-11-12 16:57:41 +00:00
Send_Nukez
ec84ad0c79 remove raw-loader and add resolve alias for svgs 2021-11-12 16:25:09 +01:00
Send_Nukez
5256ff6b7a add textarea config type, refactor config a bit and improve checkbox styles to be more inline with other inputs 2021-11-12 02:48:14 +01:00
Julien
e00027bb69
workaround to remove gradient near album info #103 2021-11-11 23:31:10 +01:00
Send_Nukez
b5033f8872 refactor ConfigMenu to use jquery 2021-11-11 22:56:25 +01:00
Send_Nukez
574a8332aa Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2021-11-11 22:55:35 +01:00
Send_Nukez
7b393b7873 fix slider tooltip being too low 2021-11-11 21:53:36 +00:00
Send_Nukez
4aa0998924 fix slider tooltip being too low 2021-11-11 22:53:16 +01:00
Send_Nukez
6de1a4e9e0 Merge branch 'main' of https://github.com/JulienMaille/dribbblish-dynamic-theme 2021-11-11 21:42:28 +01:00
Send_Nukez
d078c29ab6 update changelog 2021-11-11 20:41:44 +00:00
Send_Nukez
d4780d6220 update changelog 2021-11-11 21:41:23 +01:00
Send_Nukez
8b2a366c95 update changelog 2021-11-11 19:46:02 +01:00
Send_Nukez
eb60df1a9a fix typos in readme 2021-11-11 19:44:32 +01:00
Send_Nukez
48d57498aa add markdown styles and change the way the config backdrop is shown so notifications are above it 2021-11-11 19:34:48 +01:00
Send_Nukez
8b2d6fa141 fix #107 2021-11-11 19:26:30 +01:00
Send_Nukez
19aeb86204 remove redundant setting of color vars and rename rgb vars back to default spicetify names 2021-11-11 19:10:13 +01:00
Send_Nukez
d1d15d2425 add spiceFont mixin 2021-11-11 01:23:21 +01:00
Send_Nukez
fc6ac2e35c add markdown parsing to settings descriptions 2021-11-11 00:31:09 +01:00
Send_Nukez
5404036ee4 add more font-weights so fonts are not rendered blurry 2021-11-11 00:11:33 +01:00
Send_Nukez
1131ccbf91 refactor a tiny bit 2021-11-10 23:02:26 +01:00
Send_Nukez
2e00ea05a0 add descriptions to bug report and changelog buttons 2021-11-10 01:02:17 +00:00
Send_Nukez
0e1e89fdec channge config.export to only return keys, not areas 2021-11-10 01:54:26 +01:00
Send_Nukez
81ac31ae25 update bug report template 2021-11-09 20:48:02 +01:00
Send_Nukez
d78848c29f update changelog 2021-11-09 20:45:46 +01:00
Send_Nukez
c272dcb788 add Report Bug and Changelog buttons to the about 2021-11-09 20:43:12 +01:00
Send_Nukez
2b41bd25cc remove titles from issue templates as they are redundant with tags 2021-11-09 20:40:20 +01:00
github-actions
aa33fe1b42 Release v3.1.1 2021-11-09 14:09:51 +00:00
Send_Nukez
6f7096baa3 fix #102 2021-11-09 15:06:26 +01:00
Julien
1447fd9f3d Show spicetify version in About 2021-11-08 21:53:58 +01:00
github-actions
6118e1d9e7 Release v3.1.0 2021-11-08 19:07:57 +00:00
66 changed files with 12256 additions and 1287 deletions

3
.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"presets": ["@babel/env"]
}

View file

@ -1,35 +1,39 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
about: Create a report to help us improve (After v3.1.1, preferably open this report from inside the Dribbblish settings `About > Report Bugs` as it adds additional info automatically)
title: ""
labels: ["bug"]
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
Steps to reproduce the behavior:
1.
**Screenshots**
If applicable, add screenshots to help explain your problem.
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop Setup**
- Operating System: [e.g. Windows 10, MacOS 10.14]
- Spotify version [e.g. 1.1.71.560] from `Menu > Help > About`
- Spicetify version [e.g. 2.4.1] run `spicetify -v`
- Dribbblish version [e.g. 3.0.1] from `Dribblish Settings > About`
- Operating System: <!-- [e.g. Windows 10, MacOS 10.14] -->
- Spotify: <!-- [e.g. 1.1.71.560] from `Menu (Three dots top left) > Help > About` -->
- Spicetify: <!-- [e.g. 2.4.1] run `spicetify -v` -->
- Dribbblish: <!-- [e.g. 3.0.1] from `Dribbblish Settings > About` -->
**Logs**
Add logs from console. To do that
1. Run `spicetify enable-devtool` in terminal
2. Spotify will be restarted
3. Hit <kbd>Ctrl + Shift + I</kbd> to open DevTools window
4. Navigate to tab Console
5. Copy console window content.
<!--
Add logs from console. To do that
1. Run `spicetify enable-devtool` in terminal
2. Spotify will be restarted
3. Hit <kbd>Ctrl + Shift + I</kbd> to open DevTools window
4. Navigate to tab Console
5. Copy console window content.
-->
<!-- Paste logs below or attach a screenshot -->
```console
(Please paste here console logs or attach a screenshot)
[paste logs here]
```

View file

@ -1,20 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] "
title: ""
labels: ["enhancement"]
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
Add any other context or screenshots about the feature request here.
<!-- Add any other context or screenshots about the feature request here. -->

View file

@ -12,12 +12,38 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
with:
repository: ${{ github.event.repository.full_name }}
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0
- name: Put version in env
run: echo "DRIBBBLISH_VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Get commit SHA
run: echo "LATEST_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Build Webpack
run: |
npm install
npm run build
echo "${{ env.DRIBBBLISH_VERSION }}-${{ env.LATEST_SHA }}" > dist/VERSION
env:
DRIBBBLISH_VERSION: ${{ env.DRIBBBLISH_VERSION }}
COMMIT_HASH: ${{ env.LATEST_SHA }}
- name: Push to release branch
uses: s0/git-publish-subdir-action@develop
env:
REPO: self
BRANCH: release-stable
FOLDER: dist
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set package.json version
run: |
sed -i 's/"version":.*",/"version": "${{ env.DRIBBBLISH_VERSION }}",/' package.json

View file

@ -14,11 +14,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
with:
repository: ${{ github.event.repository.full_name }}
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Prettify code
uses: creyD/prettier_action@v4.0
uses: creyD/prettier_action@v4.1.1
with:
prettier_options: --write **/*.{js,css}
same_commit: True

View file

@ -28,13 +28,13 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
node-version: "15"
- name: Build Webpack
run: |
npm install
npm run build
echo "${{ github.event.inputs.version }}" > dist/VERSION
echo "${{ github.event.inputs.version }}-${{ env.LATEST_SHA }}" > dist/VERSION
env:
DRIBBBLISH_VERSION: ${{ github.event.inputs.version }}
COMMIT_HASH: ${{ env.LATEST_SHA }}

21
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days.'
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 15 days with no activity.'
days-before-issue-stale: 60
days-before-pr-stale: 60
days-before-issue-close: 7
days-before-pr-close: 15
operations-per-run: 100
ascending: true

View file

@ -10,6 +10,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
with:
repository: ${{ github.event.repository.full_name }}
ref: ${{ github.head_ref }}
- name: Get commit SHA
@ -18,18 +19,28 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
node-version: "15"
- name: Test-Build Webpack
run: |
npm install
npm run build
echo "Dev" > dist/VERSION
echo "Beta-${{ env.LATEST_SHA }}" > dist/VERSION
env:
DRIBBBLISH_VERSION: Beta
COMMIT_HASH: ${{ env.LATEST_SHA }}
- name: Push to beta-release branch
if: github.event_name == 'push'
uses: s0/git-publish-subdir-action@develop
env:
REPO: self
BRANCH: release-beta
FOLDER: dist
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: DribbblishDynamic_vDev-${{ env.LATEST_SHA }}
name: DribbblishDynamic_vBeta-${{ env.LATEST_SHA }}
path: dist/**

View file

@ -1,23 +1,13 @@
Added:
- Ability to hide album name and year in playbar (#85)
- Ability to change Sidebar gap sizes (#90)
- Ability to toggle playbar shadow (#92)
- Uninstall script for MacOs/Linux
- Reset button for Settings
- Ability to hide premium features in addition to ads
- Ability to show current artist's genres
Fixed:
- Some sidebar items having wrong width on hover [(this)](https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/87#issuecomment-954305428)
- Broken sidebar hover effect with unsticked items (#69)
- `Sidebar config` buttons being below the text
- "Liked Songs" having an playing indicator wich looks bad (#87)
- Queue icon is hidden (#73) (Temporary until a better solution is found)
- Playbar album name is now white again
- Color sometimes not changing until restart
- Custom search input not being focussed after clicking
- When changing songs, the color shifts to blue for a second (#179)
Improved:
- Background image now dosen't leak out of the main center area
- Sidebar playlist icon images are now properly displayed and won't look stretched anymore
- Better install script for MacOs/Linux
- `About` settings area is always open now
- Context menus are now glassy
- User menu hover state
- Add `embedWidgetGenerator` modals to custom modal styles. (Things like the [spicetify-marketplace](https://github.com/CharlieS1103/spicetify-marketplace) options)
- Changed custom app tab styles to fit in with the theme
- Changed notification styles to fit in with other elements of the theme
- The visualizer icon when playing a song from a playlist is now colored

View file

@ -1,29 +1,32 @@
# Dribbblish Dynamic
A theme for [Spicetify](https://github.com/khanhas/spicetify-cli)
<a href="https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest"><img src="https://img.shields.io/github/release/JulienMaille/dribbblish-dynamic-theme/all.svg"></a>
<a href="https://github.com/JulienMaille/dribbblish-dynamic-theme/releases"><img src="https://img.shields.io/github/downloads/JulienMaille/dribbblish-dynamic-theme/total.svg"></a>
### Preview
<img src="showcase-images/preview.gif" alt="img" width="500px">
<img src="https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/showcase-images/preview.gif" alt="img" width="500px">
## Features
### Resizable sidebar
<img src="showcase-images/resize-sidebar.png" alt="img" width="500px">
<img src="https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/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="showcase-images/customize-sidebar.png" alt="img" width="500px">
<img src="https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/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="showcase-images/playlist-folders.gif" alt="img" width="500px">
<img src="https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/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.
In profile menu, toggle option "Right expanded cover" to change expanded current track cover image to left or right side, wherever you prefer.
## Install / Update
Make sure you are using spicetify >= v2.6.0 and Spotify >= v1.1.67.
@ -42,7 +45,7 @@ curl -fsSL https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-the
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. Add the 2 lines in Patch section of the config file (see details below)
4. Add the 2 lines in `[Patch]` section of the config file (see details below)
5. Run:
```
spicetify config extensions dribbblish-dynamic.js
@ -53,7 +56,7 @@ curl -fsSL https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-the
```
## 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:
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 explicitly 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]
xpui.js_find_8008 = ,(\w+=)32,
@ -62,13 +65,20 @@ xpui.js_repl_8008 = ,${1}58,
## 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.
To edit a taskbar shortcut, right click it, then right click Spotify in the list again.
<img src="showcase-images/windows-shortcut-instruction.png" alt="img">
<img src="https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/showcase-images/windows-shortcut-instruction.png" alt="img">
In addition to `--transparent-window-controls` you can set `Windows Top Bars` to `Solid` or `Transparent` to look like this:
<img src="showcase-images/top-bars.png" alt="img" width="500px">
<img src="https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/showcase-images/top-bars.png" alt="img" width="500px">
## Follow system dark/light theme (Powershell)
Automatic dark mode should work on MacOs and Linux out of the box.
From Spotify > v1.1.70, dark mode is forced in Windows builds. You will need to patch Spotify.exe using this script:
```powershell
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/master/patch-dark-mode.ps1" | Invoke-Expression
```
## Uninstall
### Windows (PowerShell)
@ -85,6 +95,6 @@ curl -fsSL https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-the
1. Remove Patch lines you added in config file earlier.
2. Run:
```
spicetify config extensions dribbblish-dynamic.js-
spicetify config current_theme " " extensions dribbblish-dynamic.js-
spicetify apply
```

View file

@ -117,12 +117,12 @@ xpui.js_repl_8008=,`${1}58,
Write-Part "APPLYING";
$backupVer = $configFile -match "^version"
$version = ConvertFrom-StringData $backupVer[0]
if ($version.version.Length -gt 0) {
if ($backupVer.Length -gt 0) {
spicetify apply
} else {
spicetify backup apply
}
Write-Done
}
else {
Write-Part "`nYour Powershell version is less than "; Write-Emphasized "$PSMinVersion";

22
manifest.json.rem Normal file
View file

@ -0,0 +1,22 @@
[
{
"name": "Dribbblish Dynamic",
"description": "A mod of Dribbblish theme for Spicetify with support for light/dark modes and album art based colors. (Beta Release)",
"branch": "release-stable",
"preview": "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/showcase-images/preview.gif",
"readme": "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/README.md",
"usercss": "user.css",
"schemes": "color.ini",
"include": ["https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/release-stable/dribbblish-dynamic.js"]
},
{
"name": "Dribbblish Dynamic (Beta)",
"description": "A mod of Dribbblish theme for Spicetify with support for light/dark modes and album art based colors. (Beta Release)",
"branch": "release-beta",
"preview": "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/showcase-images/preview.gif",
"readme": "https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/main/README.md",
"usercss": "user.css",
"schemes": "color.ini",
"include": ["https://raw.githubusercontent.com/JulienMaille/dribbblish-dynamic-theme/release-beta/dribbblish-dynamic.js"]
}
]

9109
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,34 @@
{
"name": "dribbblish-dynamic-theme",
"version": "3.0.1",
"version": "4.1.1",
"homepage": "https://github.com/JulienMaille/dribbblish-dynamic-theme",
"bugs": {
"url": "https://github.com/JulienMaille/dribbblish-dynamic-theme/issues"
},
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.5",
"@babel/register": "^7.16.5",
"@material-icons/svg": "^1.0.22",
"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": "^1.43.5",
"sass-loader": "^12.2.0",
"webpack": "^5.58.2",
"webpack-cli": "^4.9.0"
},
"scripts": {
"build": "webpack --mode=development"
"build": "webpack"
},
"dependencies": {
"chroma-js": "^2.1.2",
"colorthief": "^2.3.2",
"jquery": "^3.6.0",
"moment": "^2.29.1",
"node-vibrant": "3.1.4",
"raw-loader": "^4.0.2"
"lodash.debounce": "^4.0.8",
"lodash.defaultsdeep": "^4.6.1",
"markdown-it": "^12.3.2",
"markdown-it-attrs": "^4.1.0",
"moment": "^2.29.2",
"node-vibrant": "^3.1.6",
"svgson": "^5.2.1"
}
}
}

39
patch-dark-mode.ps1 Normal file
View file

@ -0,0 +1,39 @@
Get-Process -Name Spotify -ErrorAction SilentlyContinue | Stop-Process -Force
Get-Process -Name SpotifyWebHelper -ErrorAction SilentlyContinue | Stop-Process -Force
$sp = spicetify config spotify_path
$sp += "\Spotify.exe"
Copy-Item $sp ($sp + ".backup")
$bytes = [System.IO.File]::ReadAllBytes($sp);
$toRemove = [System.Text.Encoding]::UTF8.GetBytes("force-dark-mode");
$sw = [System.Diagnostics.Stopwatch]::StartNew()
for ($i = 0; $i -lt $bytes.Length; $i++) {
if ($sw.Elapsed.TotalMilliseconds -ge 1000) {
Write-Progress -Activity "Enabling dark mode in Spotify.exe" -status "Patching binary file $i" -percentComplete ($i / $bytes.Length*100);
$sw.Reset();
$sw.Start();
}
$found = $true
for ($j = 0; $j -lt $toRemove.Length; $j++) {
if ($bytes[$i + $j] -ne $toRemove[$j]) {
$found = $false;
break;
}
}
if ($found -eq $true) {
for ($j = 0; $j -lt $toRemove.Length; $j++) {
$bytes[$i + $j] = [byte]65;
}
break;
}
}
[System.IO.File]::WriteAllBytes($sp, $bytes);
if ( $found ) {
Write-Host "The patch is complete." -ForegroundColor "Green"
} else {
Write-Host "Failed to patch the file." -ForegroundColor "Red"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/fonts/Roboto-Black.ttf Normal file

Binary file not shown.

BIN
src/fonts/Roboto-Bold.ttf Normal file

Binary file not shown.

BIN
src/fonts/Roboto-Light.ttf Normal file

Binary file not shown.

BIN
src/fonts/Roboto-Medium.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
src/fonts/Roboto-Thin.ttf Normal file

Binary file not shown.

131
src/icons/equaliser.svg Normal file
View file

@ -0,0 +1,131 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 24'>
<defs>
<style>
@keyframes play {
0% {
transform: scaleY(1);
}
3.3% {
transform: scaleY(0.9583);
}
6.6% {
transform: scaleY(0.9166);
}
9.9% {
transform: scaleY(0.8333);
}
13.3% {
transform: scaleY(0.7083);
}
16.6% {
transform: scaleY(0.5416);
}
19.9% {
transform: scaleY(0.4166);
}
23.3% {
transform: scaleY(0.25);
}
26.6% {
transform: scaleY(0.1666);
}
29.9% {
transform: scaleY(0.125);
}
33.3% {
transform: scaleY(0.125);
}
36.6% {
transform: scaleY(0.1666);
}
39.9% {
transform: scaleY(0.1666);
}
43.3% {
transform: scaleY(0.2083);
}
46.6% {
transform: scaleY(0.2916);
}
49.9% {
transform: scaleY(0.375);
}
53.3% {
transform: scaleY(0.5);
}
56.6% {
transform: scaleY(0.5833);
}
59.9% {
transform: scaleY(0.625);
}
63.3% {
transform: scaleY(0.6666);
}
66.6% {
transform: scaleY(0.6666);
}
69.9% {
transform: scaleY(0.6666);
}
73.3% {
transform: scaleY(0.6666);
}
76.6% {
transform: scaleY(0.7083);
}
79.9% {
transform: scaleY(0.75);
}
83.3% {
transform: scaleY(0.8333);
}
86.6% {
transform: scaleY(0.875);
}
89.9% {
transform: scaleY(0.9166);
}
93.3% {
transform: scaleY(0.9583);
}
96.6% {
transform: scaleY(1);
}
}
svg[icon-type="dribbblish"][icon-name="equaliser"] > rect[i="0"] {
transform-origin: bottom;
animation: play 0.9s -0.51s infinite;
}
svg[icon-type="dribbblish"][icon-name="equaliser"] > rect[i="1"] {
transform-origin: bottom;
animation: play 0.9s infinite;
}
svg[icon-type="dribbblish"][icon-name="equaliser"] > rect[i="2"] {
transform-origin: bottom;
animation: play 0.9s -0.15s infinite;
}
svg[icon-type="dribbblish"][icon-name="equaliser"] > rect[i="3"] {
transform-origin: bottom;
animation: play 0.9s -0.75s infinite;
}
</style>
</defs>
<rect fill="currentColor" i="0" class='cls-1' width='4' height='24'/>
<rect fill="currentColor" i="1" class='cls-1' x='6' width='4' height='24'/>
<rect fill="currentColor" i="2" class='cls-1' x='12' width='4' height='24'/>
<rect fill="currentColor" i="3" class='cls-1' x='18' width='4' height='24'/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

42
src/icons/loading.svg Normal file
View file

@ -0,0 +1,42 @@
<svg viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
/* From https://codepen.io/mrrocks/pen/EiplA */
@keyframes loader-rotator {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(270deg);
}
}
@keyframes loader-dash {
0% {
stroke-dashoffset: 187;
}
50% {
stroke-dashoffset: 46.75;
transform: rotate(135deg);
}
100% {
stroke-dashoffset: 187;
transform: rotate(450deg);
}
}
svg[icon-type="dribbblish"][icon-name="loading"] {
animation: loader-rotator 1.4s linear infinite;
}
svg[icon-type="dribbblish"][icon-name="loading"] circle {
stroke-dasharray: 187;
stroke-dashoffset: 0;
transform-origin: center;
animation: loader-dash 1.4s ease-in-out infinite;
}
</style>
</defs>
<circle fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

3
src/icons/spicetify.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8700 8700">
<path d="M3659 8515 c-3 -2 -44 -9 -92 -14 -231 -27 -564 -143 -797 -278 -657 -384 -1069 -1019 -1149 -1773 -6 -52 -15 -126 -20 -165 -24 -162 -8 -434 39 -673 35 -174 149 -498 270 -767 284 -628 398 -939 435 -1180 61 -404 -59 -689 -349 -830 -99 -48 -126 -72 -126 -112 0 -40 49 -60 164 -68 142 -9 209 4 341 68 297 145 495 444 525 792 11 127 24 173 55 187 42 19 69 1 175 -118 142 -160 380 -392 530 -517 155 -128 319 -246 705 -504 425 -285 626 -443 851 -670 191 -193 347 -439 408 -644 34 -113 58 -298 52 -398 -12 -212 -30 -300 -100 -471 -62 -150 -68 -176 -49 -195 19 -20 29 -19 88 9 50 24 169 138 251 241 180 225 320 545 381 870 27 142 28 164 28 430 0 258 -3 293 -28 448 -81 490 -273 1011 -570 1549 -193 350 -241 472 -241 618 -1 89 24 146 76 178 80 49 163 36 240 -36 51 -48 91 -104 199 -284 104 -172 159 -233 292 -319 58 -37 217 -95 332 -120 78 -17 343 -13 407 6 71 21 90 33 86 58 -4 30 -49 53 -160 81 -124 33 -184 57 -262 106 -73 46 -191 165 -241 245 -90 140 -160 360 -191 600 -20 155 -15 430 11 650 23 188 31 549 16 695 -36 352 -122 701 -234 955 -238 537 -689 971 -1247 1197 -153 63 -389 124 -565 147 -78 11 -527 16 -536 6z m1241 -1230 c44 -23 70 -62 70 -107 0 -70 -21 -102 -97 -146 -430 -250 -899 -356 -1468 -332 -221 10 -373 27 -551 64 -66 14 -137 28 -158 31 -50 8 -102 58 -111 107 -10 50 23 121 65 143 39 20 97 17 255 -14 213 -41 587 -77 706 -68 320 25 438 42 621 93 148 40 239 77 430 174 146 74 184 83 238 55z m257 -610 c88 -40 124 -131 85 -216 -27 -61 -47 -77 -212 -162 -289 -150 -581 -245 -920 -301 -390 -64 -657 -68 -1070 -15 -81 10 -297 55 -407 85 -57 16 -106 52 -128 95 -24 45 -19 124 9 166 38 55 72 73 139 73 53 0 235 -35 317 -60 37 -12 132 -23 345 -41 229 -19 541 -1 785 46 283 54 579 153 756 251 112 63 200 102 229 103 11 0 44 -10 72 -24z m276 -696 c49 -13 105 -63 128 -114 38 -83 13 -185 -59 -245 -66 -55 -329 -175 -532 -244 -226 -77 -474 -137 -685 -165 -44 -6 -114 -16 -155 -22 -41 -6 -183 -17 -315 -24 -467 -27 -957 21 -1309 128 -118 36 -166 96 -166 208 0 85 31 138 102 173 61 30 119 33 205 11 110 -29 268 -63 348 -74 44 -6 114 -15 156 -21 95 -14 779 -13 834 0 22 5 60 10 84 10 43 0 104 9 256 36 78 15 127 25 268 59 134 32 367 117 513 186 242 116 253 119 327 98z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

3
src/icons/spotify.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
<path fill="currentColor" d="M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8zm100.7 364.9c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4zm26.9-65.6c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm31-76.2c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View file

@ -1,10 +1,15 @@
import svgUndo from "../svg/undo.svg";
import $ from "jquery";
import { renderMD, defaults } from "./Util";
import { icons } from "./Icons";
export default class ConfigMenu {
/** @typedef {"checkbox" | "select" | "button" | "slider" | "number" | "text" | "textarea" | "time" | "color"} DribbblishConfigType */
/**
* @typedef {Object} DribbblishConfigItem
* @property {"checkbox" | "select" | "button" | "slider" | "number" | "text" | "time" | "color"} type
* @property {String|DribbblishConfigArea} [area={name: "Main Settings", order: 0}]
* @property {DribbblishConfigType} type
* @property {String | DribbblishConfigArea} [area={name: "Main Settings", order: 0}]
* @property {any} [data={}]
* @property {Number} [order=0] order < 0 = Higher up | order > 0 = Lower Down
* @property {String} key
@ -12,13 +17,16 @@ export default class ConfigMenu {
* @property {String} [description=""]
* @property {any} [defaultValue]
* @property {Boolean} [hidden=false]
* @property {Boolean} [resetButton=true]
* @property {Boolean} [insertOnTop=false]
* @property {Boolean} [fireInitialChange=true]
* @property {Boolean} [save=true]
* @property {validate} [validate]
* @property {showChildren} [showChildren]
* @property {onAppended} [onAppended]
* @property {onChange} [onChange]
* @property {DribbblishConfigItem[]} [children=[]]
* @property {String} [childOf=null] key of parent (set automatically)
* @property {String} [parent=null] key of parent (set automatically)
*/
/**
@ -28,6 +36,13 @@ export default class ConfigMenu {
* @property {Boolean} [toggleable=true]
*/
/**
* @callback validate
* @this {DribbblishConfigItem}
* @param {any} value
* @returns {Boolean | String}
*/
/**
* @callback showChildren
* @this {DribbblishConfigItem}
@ -51,82 +66,38 @@ export default class ConfigMenu {
/** @type {Object.<string, DribbblishConfigItem>} */
#config;
/** @type {Spicetify.Menu.Item} */
#configButton;
constructor() {
this.#config = {};
this.configButton = new Spicetify.Menu.Item("Dribbblish Settings", false, () => this.open());
this.configButton.register();
this.#configButton = new Spicetify.Menu.Item("Dribbblish Settings", false, () => this.open());
this.#configButton.register();
const container = document.createElement("div");
container.id = "dribbblish-config";
container.innerHTML = /* html */ `
<div class="dribbblish-config-container">
<button aria-label="Close" class="dribbblish-config-close main-trackCreditsModal-closeBtn">
<svg width="18" height="18" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M31.098 29.794L16.955 15.65 31.097 1.51 29.683.093 15.54 14.237 1.4.094-.016 1.508 14.126 15.65-.016 29.795l1.414 1.414L15.54 17.065l14.144 14.143" fill="currentColor" fill-rule="evenodd"></path></svg>
</button>
<button aria-label="Close" class="dribbblish-config-close main-trackCreditsModal-closeBtn">${icons.get("close", { size: 24 })}</button>
<h1>Dribbblish Settings</h1>
<input type="search" placeholder="Search" class="dribbblish-config-search">
<div class="dribbblish-config-areas"></div>
</div>
<div class="dribbblish-config-backdrop"></div>
`;
document.body.appendChild(container);
document.querySelector(".dribbblish-config-close").addEventListener("click", () => this.close());
document.querySelector(".dribbblish-config-backdrop").addEventListener("click", () => this.close());
$(".dribbblish-config-close").on("click", () => this.close());
$(".dribbblish-config-backdrop").on("click", () => this.close());
$(".dribbblish-config-search").on("input", (e) => this.#search(e.target.value));
}
open() {
document.getElementById("dribbblish-config").setAttribute("active", "");
$("#dribbblish-config").attr("active", true);
}
close() {
document.getElementById("dribbblish-config").removeAttribute("active");
}
/**
* @private
* @param {DribbblishConfigItem} options
*/
addInputHTML(options) {
this.registerArea(options.area);
const parent = document.querySelector(`.dribbblish-config-area[name="${options.area.name}"] .dribbblish-config-area-items`);
const elem = document.createElement("div");
elem.style.order = options.order;
elem.classList.add("dribbblish-config-item");
elem.setAttribute("key", options.key);
elem.setAttribute("type", options.type);
elem.setAttribute("hidden", options.hidden);
if (options.childOf) elem.setAttribute("parent", options.childOf);
elem.innerHTML = /* html */ `
<h2 class="x-settings-title main-type-cello${!options.description ? " no-desc" : ""}" as="h2">
${options.name}
${["button"].includes(options.type) ? "" : /* html */ `<button aria-label="Reset" class="dribbblish-config-item-reset main-trackCreditsModal-closeBtn">${svgUndo}</button>`}
</h2>
<label class="main-type-mesto">${options.description.replace(/\n/g, "<br>")}</label>
<label class="x-toggle-wrapper x-settings-secondColumn">
${options.input}
</label>
`;
if (options.insertOnTop && parent.children.length > 0) {
parent.insertBefore(elem, parent.children[0]);
} else {
parent.appendChild(elem);
}
const resetButton = elem.querySelector(".dribbblish-config-item-reset");
if (resetButton) {
elem.querySelector(".dribbblish-config-item-reset").addEventListener("click", () => {
this.reset(options.key);
const defaultVal = this.get(options.key);
if (options.type == "checkbox") {
elem.querySelector("input").checked = defaultVal;
} else {
elem.querySelector("input, select").value = defaultVal;
}
options.onChange(defaultVal);
});
}
$("#dribbblish-config").removeAttr("active");
}
/**
@ -141,16 +112,20 @@ export default class ConfigMenu {
data: {},
name: "",
description: "",
hidden: false,
resetButton: true,
insertOnTop: false,
fireInitialChange: true,
save: true,
validate: () => true,
showChildren: () => true,
onAppended: () => {},
onChange: () => {},
children: [],
childOf: null
parent: null
};
// Set Defaults
options = { ...defaultOptions, ...options };
options = defaults(options, defaultOptions);
if (typeof options.area == "string") options.area = { name: options.area, order: 0 };
options.description = options.description
.split("\n")
@ -159,163 +134,173 @@ export default class ConfigMenu {
.join("\n");
options._onChange = options.onChange;
options.onChange = (val) => {
const isValid = validate(val) === true;
$(`.dribbblish-config-item[key="${options.key}"]`).attr("changed", options.type != "button" && isValid && val != options.defaultValue ? "" : null);
if (!isValid) return;
this.set(options.key, val, options.save);
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) => {
return { ...child, area: options.area, childOf: options.key };
return { ...child, area: options.area, parent: options.key, order: options.order ?? 0 + child.order ?? 0 };
});
this.#config[options.key] = options;
function validate(val) {
const isValid = options.validate.call(options, val);
const $elem = $(`.dribbblish-config-item[key="${options.key}"]`);
if (isValid === true) {
$elem.attr("invalid", null).css("--validation-error", "");
} else {
const error = isValid === false ? "Invalid" : isValid;
$elem.attr("invalid", "").css("--validation-error", `"${error.replace(/"/g, `\\"`)}"`);
}
return isValid;
}
let elem;
let input;
if (options.type == "checkbox") {
const input = /* html */ `
<input id="dribbblish-config-input-${options.key}" class="x-toggle-input" type="checkbox"${this.get(options.key) ? " checked" : ""}>
<span class="x-toggle-indicatorWrapper">
<span class="x-toggle-indicator"></span>
</span>
`;
this.addInputHTML({ ...options, input });
elem = document.createDocumentFragment();
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("change", (e) => {
this.set(options.key, e.target.checked);
options.onChange(this.get(options.key));
input = document.createElement("input");
input.classList.add("dribbblish-config-input");
input.type = "checkbox";
input.classList.add("x-toggle-input");
input.checked = this.get(options.key);
input.addEventListener("change", (e) => {
options.onChange(e.target.checked);
});
elem.appendChild(input);
const indicator = document.createElement("span");
indicator.classList.add("x-toggle-indicatorWrapper");
indicator.innerHTML = /* html */ `<span class="x-toggle-indicator"></span>`;
elem.appendChild(indicator);
} else if (options.type == "select") {
// Validate
const val = this.get(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}">
${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, e.target.value);
options.onChange(this.get(options.key));
input = document.createElement("select");
input.classList.add("dribbblish-config-input");
input.classList.add("main-dropDown-dropDown");
input.innerHTML = Object.entries(options.data)
.map(([key, name]) => `<option value="${key}"${this.get(options.key) == key ? " selected" : ""}>${name}</option>`)
.join("");
input.addEventListener("change", (e) => {
options.onChange(e.target.value);
});
} else if (options.type == "button") {
options.fireInitialChange = false;
if (typeof options.data != "string") options.data = options.name;
options.fireInitialChange = false;
options.resetButton = false;
options.save = false;
const input = /* html */ `
<button class="main-buttons-button main-button-primary" type="button" id="dribbblish-config-input-${options.key}">
<div class="x-settings-buttonContainer">
<span>${options.data}</span>
</div>
</button>
`;
this.addInputHTML({ ...options, input });
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("click", (e) => {
input = document.createElement("button");
input.classList.add("dribbblish-config-input");
input.type = "button";
input.classList.add("main-buttons-button", "main-button-primary");
input.innerHTML = /* html */ `<div class="x-settings-buttonContainer"><span>${options.data}</span></div>`;
input.addEventListener("click", (e) => {
options.onChange(true);
});
} else if (options.type == "number") {
// Validate
if (options.defaultValue == null) options.defaultValue = 0;
const val = this.get(options.key);
if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min);
if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max);
const _val = this.get(options.key);
if (options.data.min != null && _val < options.data.min) this.set(options.key, options.data.min, options.save);
if (options.data.max != null && _val > options.data.max) this.set(options.key, options.data.max, options.save);
const input = /* html */ `
<input
type="number"
id="dribbblish-config-input-${options.key}"
${options.data.min != null ? `min="${options.data.min}"` : ""}
${options.data.max != null ? `max="${options.data.max}"` : ""}
step="${options.data.step ?? 1}"
value="${this.get(options.key)}"
>
`;
this.addInputHTML({ ...options, input });
// Prevent inputting +, - and e. Why is it even possible in the first place?
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("keypress", (e) => {
if (["+", "-", "e"].includes(e.key)) e.preventDefault();
input = document.createElement("input");
input.classList.add("dribbblish-config-input");
input.type = "number";
input.value = this.get(options.key);
input.step = options.data.step ?? 1;
if (options.data.min != null) input.min = options.data.min;
if (options.data.max != null) input.max = options.data.max;
input.addEventListener("keypress", (e) => {
// Prevent inputting + and e.
if (["+", "e"].includes(e.key)) e.preventDefault();
});
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => {
input.addEventListener("input", (e) => {
if (options.data.min != null && e.target.value < options.data.min) e.target.value = options.data.min;
if (options.data.max != null && e.target.value > options.data.max) e.target.value = options.data.max;
this.set(options.key, Number(e.target.value));
options.onChange(this.get(options.key));
options.onChange(Number(e.target.value));
});
} else if (options.type == "text") {
if (options.defaultValue == null) options.defaultValue = "";
const input = /* html */ `
<input type="text" id="dribbblish-config-input-${options.key}" value="${this.get(options.key)}">
`;
this.addInputHTML({ ...options, input });
input = document.createElement("input");
input.classList.add("dribbblish-config-input");
input.type = "text";
input.value = this.get(options.key);
input.addEventListener("input", (e) => {
options.onChange(e.target.value);
});
} else if (options.type == "textarea") {
if (options.defaultValue == null) options.defaultValue = "";
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => {
// TODO: maybe add an validation function via `data.validate`
this.set(options.key, e.target.value);
options.onChange(this.get(options.key));
input = document.createElement("textarea");
input.classList.add("dribbblish-config-input");
input.value = this.get(options.key);
input.addEventListener("input", (e) => {
options.onChange(e.target.value);
});
} else if (options.type == "slider") {
// Validate
if (options.defaultValue == null) options.defaultValue = 0;
const val = this.get(options.key);
if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min);
if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max);
if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min, options.save);
if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max, options.save);
const input = /* html */ `
<input
type="range"
id="dribbblish-config-input-${options.key}"
name="${options.name}"
min="${options.data?.min ?? "0"}"
max="${options.data?.max ?? "100"}"
step="${options.data?.step ?? "1"}"
value="${this.get(options.key)}"
tooltip="${this.get(options.key)}${options.data?.suffix ?? ""}"
>
`;
this.addInputHTML({ ...options, input });
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => {
document.getElementById(`dribbblish-config-input-${options.key}`).setAttribute("tooltip", `${e.target.value}${options.data?.suffix ?? ""}`);
document.getElementById(`dribbblish-config-input-${options.key}`).setAttribute("value", e.target.value);
this.set(options.key, Number(e.target.value));
options.onChange(this.get(options.key));
input = document.createElement("input");
input.classList.add("dribbblish-config-input");
input.type = "range";
input.step = options.data.step ?? 1;
input.min = options.data.min ?? 0;
input.max = options.data.max ?? 100;
input.value = this.get(options.key);
input.setAttribute("tooltip", `${this.get(options.key)}${options.data.suffix ?? ""}`);
input.addEventListener("input", (e) => {
options.onChange(Number(e.target.value));
$(`#dribbblish-config-input-${options.key}`).attr("tooltip", `${e.target.value}${options.data?.suffix ?? ""}`);
});
} else if (options.type == "time") {
// Validate
if (options.defaultValue == null) options.defaultValue = "00:00";
const input = /* html */ `
<input type="time" id="dribbblish-config-input-${options.key}" name="${options.name}" value="${this.get(options.key)}">
`;
this.addInputHTML({ ...options, input });
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => {
document.getElementById(`dribbblish-config-input-${options.key}`).setAttribute("value", e.target.value);
this.set(options.key, e.target.value);
options.onChange(this.get(options.key));
input = document.createElement("input");
input.classList.add("dribbblish-config-input");
input.type = "time";
input.value = this.get(options.key);
input.addEventListener("input", (e) => {
options.onChange(e.target.value);
});
} else if (options.type == "color") {
// Validate
if (options.defaultValue == null) options.defaultValue = "#000000";
const input = /* html */ `
<input type="color" id="dribbblish-config-input-${options.key}" name="${options.name}" value="${this.get(options.key)}">
`;
this.addInputHTML({ ...options, input });
document.getElementById(`dribbblish-config-input-${options.key}`).addEventListener("input", (e) => {
this.set(options.key, e.target.value);
options.onChange(this.get(options.key));
input = document.createElement("input");
input.classList.add("dribbblish-config-input");
input.type = "color";
input.value = this.get(options.key);
input.addEventListener("input", (e) => {
options.onChange(e.target.value);
});
} else {
throw new Error(`Config Type "${options.type}" invalid`);
}
this.#addInputHTML({ ...options, input: elem ?? input });
options.input = input;
// Re-write internal config since some values may have changed
this.#config[options.key] = options;
validate(this.get(options.key));
$(`.dribbblish-config-item[key="${options.key}"]`).attr("changed", options.save && this.get(options.key) != options.defaultValue ? "" : null);
options.children.forEach((child) => this.register(child));
options.onAppended.call(options);
@ -326,36 +311,17 @@ export default class ConfigMenu {
* @param {DribbblishConfigArea} area
*/
registerArea(area) {
if (area.toggleable == null) area.toggleable = true;
/** @type {DribbblishConfigArea} */
const defaultOptions = {
toggleable: true,
order: 0
};
area = { ...defaultOptions, ...area };
if (!document.querySelector(`.dribbblish-config-area[name="${area.name}"]`)) {
const areaElem = document.createElement("div");
areaElem.classList.add("dribbblish-config-area");
areaElem.style.order = area.order;
const uncollapsedAreas = JSON.parse(localStorage.getItem("dribbblish:config-areas:uncollapsed") ?? "[]");
if (area.toggleable && !uncollapsedAreas.includes(area.name)) areaElem.toggleAttribute("collapsed");
areaElem.setAttribute("name", area.name);
areaElem.innerHTML = /* html */ `
<h2 class="dribbblish-config-area-header">
${area.name}
${!area.toggleable ? "" : /* html */ `<svg height="24" width="24" viewBox="0 0 24 24" class="main-topBar-icon"><polyline points="16 4 7 12 16 20" fill="none" stroke="currentColor"></polyline></svg>`}
</h2>
<div class="dribbblish-config-area-items"></div>
`;
document.querySelector(".dribbblish-config-areas").appendChild(areaElem);
if (area.toggleable) {
areaElem.querySelector("h2").addEventListener("click", () => {
areaElem.toggleAttribute("collapsed");
let uncollapsedAreas = JSON.parse(localStorage.getItem("dribbblish:config-areas:uncollapsed") ?? "[]");
if (areaElem.hasAttribute("collapsed")) {
uncollapsedAreas = uncollapsedAreas.filter((areaName) => areaName != area.name);
} else {
uncollapsedAreas.push(area.name);
}
localStorage.setItem("dribbblish:config-areas:uncollapsed", JSON.stringify(uncollapsedAreas));
});
}
this.#addAreaHTML(area);
} else {
throw new Error(`Area "${area.name}" already exists`);
}
}
@ -366,10 +332,10 @@ export default class ConfigMenu {
* @returns {any}
*/
get(key, defaultValueOverride) {
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
const val = JSON.parse(this.#config[key]?.storageCache ?? localStorage.getItem(`dribbblish:config:${key}`) ?? null); // Turn undefined into null because `JSON.parse()` doesn't like undefined
if (val == null || val?.type != this.#config[key]?.type) {
localStorage.removeItem(`dribbblish:config:${key}`);
return defaultValueOverride ?? this.#config[key].defaultValue;
return defaultValueOverride ?? this.#config[key]?.defaultValue;
}
return val.value;
}
@ -378,11 +344,12 @@ export default class ConfigMenu {
*
* @param {String} key
* @param {any} val
* @param {Boolean} [save=true] if setting should be stored in localStorage
*/
set(key, val) {
set(key, val, save = true) {
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));
if (save) localStorage.setItem(`dribbblish:config:${key}`, JSON.stringify(val));
}
/**
@ -392,19 +359,203 @@ export default class ConfigMenu {
reset(key) {
delete this.#config[key].storageCache;
localStorage.removeItem(`dribbblish:config:${key}`);
const options = this.#config[key];
const defaultVal = this.get(options.key);
if (options.type == "checkbox") {
options.input.checked = defaultVal;
} else {
options.input.value = defaultVal;
if (options.type == "slider") options.input.setAttribute("tooltip", `${defaultVal}${options.data.suffix ?? ""}`);
}
options.onChange(defaultVal);
}
/**
*
* @param {String} key
* @param {Boolean} hidden
* @private
*/
setHidden(key, hidden) {
setHidden(key, hidden, search = false) {
this.#config[key].hidden = hidden;
document.querySelector(`.dribbblish-config-item[key="${key}"]`).setAttribute("hidden", hidden);
const $elem = $(`.dribbblish-config-item[key="${key}"]`);
$elem.attr(search ? "hidden-override" : "hidden", hidden ? "" : null);
// If element has children or a parent
if ($elem.attr("parent") != null || $elem.attr("children") != null) {
// Get parent of element block
const $parent = $elem.attr("parent") != null ? $(`[children~="${key}"]`) : $elem;
const $nextChildren = $parent.nextAll(`[parent="${$parent.attr("key")}"]`);
// Make parent connect on bottom when children are visible
$parent.attr("connect-bottom", $nextChildren.filter(":not([hidden]):not([hidden-override])").length > 0 ? "" : null);
// Reset all children's bottom connection
$nextChildren.each(function () {
$(this).attr("connect-bottom", null);
});
// Add bottom connection to all but the last visible child
$nextChildren
.filter(":not([hidden]):not([hidden-override])")
.slice(0, -1)
.each(function () {
$(this).attr("connect-bottom", "");
});
//* NOTE: All children automatically have a top connection
}
}
setDisabled(key, disabled) {
this.#config[key].input.disabled = disabled;
}
getOptions(key) {
return this.#config[key];
}
export() {
const obj = {
EXPORTED_WITH: process.env.DRIBBBLISH_VERSION
};
Object.entries(this.#config).forEach(([key, options]) => {
if (options.save) obj[key] = this.get(key);
});
return obj;
}
/**
* @param {String} text
*/
#search(text) {
text = text.trim().toLowerCase();
$(".dribbblish-config-area-header svg").css("display", text != "" ? "none" : "");
$(".dribbblish-config-area").attr("search", text != "" ? "" : null);
const cmp = (s) => s.trim().toLowerCase().includes(text.trim().toLowerCase());
/**
* @param {DribbblishConfigItem} options
*/
function cmpOpts(options) {
let matches = cmp(options.name) || cmp($(`.dribbblish-config-item[key="${options.key}"] .dribbblish-config-item-header label`).text());
if (!matches && options.type == "select") matches = Object.values(options.data).some(cmp);
return matches;
}
// Check every item
for (const [key, options] of Object.entries(this.#config)) {
this.setHidden(key, false, true);
if (text == "") continue;
const show = cmpOpts(options);
this.setHidden(key, !show, true);
}
// Check every item's children and show it when any child is visible
for (const [key, options] of Object.entries(this.#config)) {
if (options.children.length != 0 && $(`.dribbblish-config-item[parent="${options.key}"]`).filter(":not([hidden]):not([hidden-override])").length != 0) this.setHidden(key, false, true);
}
// Hide areas without visible children
for (const area of $(".dribbblish-config-area").toArray()) {
const $area = $(area);
const itemsVisible = $area.children(".dribbblish-config-area-items").children(":not([hidden]):not([hidden-override])").length;
$area.css("display", text != "" && itemsVisible == 0 ? "none" : "");
}
$(`.dribbblish-config-item`).filter(":not([hidden]):not([hidden-override])").length;
}
/**
* @private
* @param {DribbblishConfigItem} options
*/
#addInputHTML(options) {
if (!document.querySelector(`.dribbblish-config-area[name="${options.area.name}"]`)) this.registerArea(options.area);
const parent = document.querySelector(`.dribbblish-config-area[name="${options.area.name}"] .dribbblish-config-area-items`);
const elem = document.createElement("div");
if (options.order != 0) elem.style.order = options.order;
elem.classList.add("dribbblish-config-item");
elem.setAttribute("key", options.key);
elem.setAttribute("type", options.type);
if (options.hidden) elem.setAttribute("hidden", true);
if (options.parent) elem.setAttribute("parent", options.parent);
if (options.children.length > 0) elem.setAttribute("children", options.children.map((c) => c.key).join(" "));
elem.innerHTML = /* html */ `
${
options.name != null && options.description != null
? /* html */ `
<div class="dribbblish-config-item-header">
<h2 class="x-settings-title main-type-cello" as="h2" empty="${options.name == null}">${options.name}</h2>
<label class="main-type-mesto" empty="${options.description == null}" markdown>${renderMD(options.description)}</label>
</div>
`
: ""
}
<div class="dribbblish-config-item-input">
<label class="x-toggle-wrapper x-settings-secondColumn"></label>
</div>
`;
elem.querySelector(".dribbblish-config-item-input > label").appendChild(options.input);
if (options.resetButton && options.name != null && options.description != null) {
const resetBtn = document.createElement("button");
resetBtn.ariaLabel = "Reset";
resetBtn.className = "dribbblish-config-item-reset main-trackCreditsModal-closeBtn";
resetBtn.innerHTML = icons.get("delete-outline", { size: 20, title: "Reset Setting" });
resetBtn.addEventListener("click", () => {
this.reset(options.key);
});
elem.querySelector(".dribbblish-config-item-header > h2").appendChild(resetBtn);
}
if (options.insertOnTop && parent.children.length > 0) {
parent.insertBefore(elem, parent.children[0]);
} else {
parent.appendChild(elem);
}
}
/**
*
* @param {DribbblishConfigArea} area
*/
#addAreaHTML(area) {
const areaElem = document.createElement("div");
areaElem.classList.add("dribbblish-config-area");
if (area.order != 0) areaElem.style.order = area.order;
const uncollapsedAreas = JSON.parse(localStorage.getItem("dribbblish:config-areas:uncollapsed") ?? "[]");
if (area.toggleable && !uncollapsedAreas.includes(area.name)) areaElem.toggleAttribute("collapsed");
areaElem.setAttribute("name", area.name);
areaElem.innerHTML = /* html */ `
<h2 class="dribbblish-config-area-header">
${area.name}
${!area.toggleable ? "" : icons.get("expand-more", { size: 24, scale: 1.2 })}
</h2>
<div class="dribbblish-config-area-items"></div>
`;
document.querySelector(".dribbblish-config-areas").appendChild(areaElem);
if (area.toggleable) {
areaElem.querySelector("h2").addEventListener("click", () => {
if (document.querySelector(".dribbblish-config-search").value.trim() != "") return;
areaElem.toggleAttribute("collapsed");
let uncollapsedAreas = JSON.parse(localStorage.getItem("dribbblish:config-areas:uncollapsed") ?? "[]");
if (areaElem.hasAttribute("collapsed")) {
uncollapsedAreas = uncollapsedAreas.filter((areaName) => areaName != area.name);
} else {
uncollapsedAreas.push(area.name);
}
localStorage.setItem("dribbblish:config-areas:uncollapsed", JSON.stringify(uncollapsedAreas));
});
}
}
}

80
src/js/Dribbblish.js Normal file
View file

@ -0,0 +1,80 @@
import ConfigMenu from "./ConfigMenu";
import Info from "./Info";
import Loader from "./Loader";
import Overlay from "./Overlay";
import Icon, { icons } from "./Icons";
export default class Dribbblish {
/**
* @typedef {"ready"} Event
*/
/**
* @callback listener
* @param {any} [data]
* @returns {void}
*/
/** @type {ConfigMenu} */
config;
/** @type {Info} */
info;
/** @type {Loader} */
loader;
/** @type {Overlay} */
overlay;
/** @type {Icon} */
icons;
/** @type {Object.<string, listener[]>} */
#listeners = {};
/** @type {Boolean} */
#ready = false;
constructor() {
this.config = new ConfigMenu();
this.info = new Info();
this.loader = new Loader();
this.overlay = new Overlay();
this.icons = icons;
let tries = 0;
const interval = setInterval(() => {
if (++tries > 50) throw new Error("ready timeout");
if (document.querySelector("#main") == null || Spicetify?.showNotification == undefined || !this.info.isReady()) return;
this.#ready = true;
this.emit("ready");
clearInterval(interval);
}, 200);
}
/**
* @param {Event} event
* @param {any} data
*/
emit(event, data) {
this.#listeners[event]?.forEach((listener) => listener(data));
}
/**
* @param {Event} event
* @param {listener} listener
*/
on(event, listener) {
this.#listeners[event] = [...(this.#listeners[event] ?? []), listener];
if (event == "ready" && this.#ready) listener();
}
/**
* @param {Event} event
* @param {listener} listener
*/
off(event, listener) {
this.#listeners = this.#listeners[event].filter((f) => f != listener);
}
}

98
src/js/Folders.js Normal file
View file

@ -0,0 +1,98 @@
import { waitForElement, htmlToNode } from "./Util";
import { icons } from "./Icons";
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 */
async function loadPlaylistImage() {
for (const item of listElem.children) {
let link = item.querySelector("a");
if (!link) continue;
let [_, app, uid] = link.pathname.split("/");
if (link.querySelector("svg")) link.querySelector("svg").remove();
if (link.querySelector("img")) link.querySelector("img").remove();
const title = link.querySelector("span").innerText;
let elem;
if (app === "playlist") {
const {
metadata: { picture }
} = await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${Spicetify.URI.playlistV2URI(uid).toURI()}/metadata`, { policy: { picture: true } });
if (picture != null && picture.trim() != "") {
if (!link.querySelector("img")) elem = document.createElement("img");
elem.src = picture;
} else {
if (!link.querySelector("svg")) elem = htmlToNode(icons.get("note", { title }));
}
} else if (app === "folder") {
const base64 = localStorage.getItem("dribbblish:folder-image:" + uid);
if (base64 != null) {
if (!link.querySelector("img")) elem = document.createElement("img");
elem.src = base64;
} else {
if (!link.querySelector("svg")) elem = htmlToNode(icons.get("folder", { title }));
}
} else {
continue;
}
elem.title = title;
elem.classList.add("playlist-picture");
link.prepend(elem);
}
}
loadPlaylistImage();
new MutationObserver(loadPlaylistImage).observe(listElem, { childList: true });
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");
}
loadPlaylistImage();
};
reader.readAsDataURL(file);
};
new Spicetify.ContextMenu.Item(
"Remove folder image",
([uri]) => {
const id = Spicetify.URI.from(uri).id;
localStorage.removeItem("dribbblish:folder-image:" + id);
loadPlaylistImage();
},
([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();
});

124
src/js/Icons.js Normal file
View file

@ -0,0 +1,124 @@
import { defaults } from "./Util";
import { parseSync as parseSVG, stringify as stringifySVG } from "svgson";
export default class Icons {
/** @typedef {"custom" | "material:baseline" | "material:outline" | "material:round" | "material:sharp" | "material:twotone"} IconStyle */
/**
* @typedef {Object} IconOptions
* @property {IconStyle} [style="round"]
* @property {String} [className=""]
* @property {Number} [size=16]
* @property {Number} [scale=1]
* @property {String} [fill="currentColor"]
* @property {Boolean} [base64=false]
*/
/** @type {Object.<String, Object.<IconStyle, String>>} */
#icons;
constructor(icons) {
this.#icons = icons ?? process.env.DRIBBBLISH_ICONS;
}
/**
* @param {String} name
* @param {IconStyle} style
* @returns {String}
*/
getRawSVG(name, style) {
if (style == null) style = this.#getDefaultStyle(name);
if (!this.#icons.hasOwnProperty(name)) throw new Error(`Icon "${name}" does not exist`);
if (!this.#icons[name].hasOwnProperty(style)) {
const styles = Object.keys(this.#icons[name])
.map((s) => `"${s}"`)
.join(", ");
throw new Error(`Icon "${name}" does not have style "${style}". It is available in styles [${styles}].`);
}
return this.#icons[name][style];
}
getAvailableStyles(name) {
if (!this.#icons.hasOwnProperty(name)) throw new Error(`Icon "${name}" does not exist`);
return Object.keys(this.#icons[name]);
}
/**
* @param {String} name
* @returns
*/
#getDefaultStyle(name) {
const styles = this.getAvailableStyles(name);
for (const s of ["custom", "material:round"]) {
if (styles.includes(s)) return s;
}
return styles[0];
}
/**
* @param {String} name icon name lowercase with dashes like `ac-unit`
* @param {IconOptions} options
* @see https://fonts.google.com/icons?selected=Material+Icons
* @returns
*/
get(name, options) {
/** @type {IconOptions} */
const defaultOptions = {
style: this.#getDefaultStyle(name),
className: "",
size: 16,
scale: 1,
fill: "currentColor",
base64: false
};
options = defaults(options, defaultOptions);
const svg = parseSVG(this.getRawSVG(name, options.style));
// Add general / required attributes
svg.attributes["icon-type"] = "dribbblish";
svg.attributes["icon-name"] = name;
svg.attributes["icon-style"] = options.style;
svg.attributes.fill = options.fill;
svg.attributes.width = options.size;
svg.attributes.height = options.size;
// Add className
if (options.className != "") svg.attributes.class = options.className;
// Add Styles
const styles = {};
if (options.scale != 1) {
styles["transform"] = `scale(${options.scale})`;
styles["transform-origin"] = "center";
}
const styleStr = Object.entries(styles)
.map(([prop, val]) => `${prop}: ${val};`)
.join(" ");
svg.children = svg.children.map((child) => {
if (styleStr != "") child.attributes.style = styleStr;
return child;
});
// Add title
if (options.title != null) {
svg.children.push({
name: "title",
type: "element",
value: "",
children: [{ name: "", type: "text", value: options.title, attributes: {}, children: [] }]
});
}
if (options.base64) {
return `data:image/svg+xml;base64,${Buffer.from(stringifySVG(svg)).toString("base64")}`;
} else {
return stringifySVG(svg);
}
}
}
export const icons = new Icons();

90
src/js/Info.js Normal file
View file

@ -0,0 +1,90 @@
import $ from "jquery";
import { icons } from "./Icons";
import { waitForElement } from "./Util";
export default class Info {
/**
* @typedef {Object} DribbblishInfo
* @property {String} [text]
* @property {String} [tooltip]
* @property {String} [icon] svg string or icon name
* @property {DribbblishInfoColor} [color]
* @property {Number} [order=0] order < 0 = More to the Left | order > 0 = More to the Right
* @property {onClick} [onClick]
*/
/**
* @typedef {Object} DribbblishInfoColor
* @property {String} [fg]
* @property {String} [bg]
*/
/**
* @callback onClick
* @returns {void}
*/
/** @type {HTMLDivElement} */
#container;
/** @type {MarkdownIt} */
#md;
/** @type {Boolean} */
#ready = false;
constructor() {
waitForElement([".main-topBar-container", ".main-userWidget-box"], ([topBarContainer, userWidget]) => {
this.#container = document.createElement("div");
this.#container.id = "dribbblish-info-container";
topBarContainer.insertBefore(this.#container, userWidget);
this.#ready = true;
});
}
isReady() {
return this.#ready;
}
/**
* @param {String} key
* @param {DribbblishInfo} info
*/
set(key, info) {
if (!this.#ready) {
setTimeout(() => this.set(key, info), 200);
return;
}
this.remove(key);
if (info == null) return;
if (info.text == null && info.icon == null) throw new Error("invalid info");
const elem = document.createElement("div");
elem.classList.add("dribbblish-info-item");
elem.addEventListener("click", info.onClick);
elem.setAttribute("key", key);
if (info.text == null) elem.setAttribute("icon-only", "");
if (info.tooltip != null) elem.setAttribute("title", info.tooltip);
if (info.onClick != null) elem.setAttribute("clickable", "");
if (info.color != null) {
const { fg, bg } = info.color;
if (fg != null) elem.style.color = fg;
if (bg != null) elem.style.backgroundColor = bg;
}
if (info.order != 0) elem.style.order = info.order;
if (!info.icon.startsWith("<svg")) info.icon = icons.get(info.icon, { size: 18 });
elem.innerHTML = `${info.text ?? ""}${info.icon ?? ""}`;
this.#container.appendChild(elem);
}
/**
* @param {String} key
*/
remove(key) {
$(this.#container).find(`.dribbblish-info-item[key="${key}"]`).remove();
}
}

18
src/js/Loader.js Normal file
View file

@ -0,0 +1,18 @@
import Overlay from "./Overlay";
export default class Loader {
/** @type {Overlay} */
#overlay;
constructor() {
this.#overlay = new Overlay();
}
show(text) {
this.#overlay.show({ icon: "loading", text });
}
hide() {
this.#overlay.hide();
}
}

68
src/js/Overlay.js Normal file
View file

@ -0,0 +1,68 @@
import { defaults } from "./Util";
import { icons } from "./Icons";
export default class Overlay {
/**
* @typedef {Object} DribbblishOverlayOptions
* @property {String} icon
* @property {String} text
* @property {DribbblishOverlayColor} color
*/
/**
* @typedef {Object} DribbblishOverlayColor
* @property {String} [icon]
* @property {String} [text]
*/
/** @type {HTMLDivElement} */
#container;
/** @type {HTMLElement} */
#iconElem;
/** @type {HTMLSpanElement} */
#textElem;
constructor() {
this.#container = document.createElement("div");
this.#container.id = "dribbblish-overlay";
this.#container.innerHTML = /* html */ `
<div>
<i></i>
<span>Loading...</span>
</div>
`;
this.#iconElem = this.#container.querySelector("i");
this.#textElem = this.#container.querySelector("span");
document.body.appendChild(this.#container);
}
/**
*
* @param {DribbblishOverlayOptions} options
*/
show(options) {
const defaultOptions = {
icon: "",
text: "",
color: {
icon: "var(--spice-text)",
text: "var(--spice-subtext)"
}
};
options = defaults(options, defaultOptions);
if (options.icon != "" && !options.icon.startsWith("<svg")) options.icon = icons.get(options.icon, { size: 64 });
this.#iconElem.innerHTML = options.icon;
this.#iconElem.style.setProperty("--color", options.color.icon);
this.#textElem.innerHTML = options.text;
this.#textElem.style.setProperty("--color", options.color.text);
this.#container.setAttribute("active", "");
}
hide() {
this.#container.removeAttribute("active");
}
}

13
src/js/SassUtil.js Normal file
View file

@ -0,0 +1,13 @@
export function lightOffset(n, offset) {
return `calc(${n} + ${offset} * var(--is_light))`;
}
export function spiceColor(key, alpha = 1, _lightOffset = 0) {
if (alpha == 1) {
return `var(--spice-${key})`;
} else if (_lightOffset == 0) {
return `rgba(var(--spice-rgb-${key}), ${alpha})`;
} else {
return `rgba(var(--spice-rgb-${key}), ${lightOffset(alpha, _lightOffset)})`;
}
}

95
src/js/Util.js Normal file
View file

@ -0,0 +1,95 @@
import MarkdownIt from "markdown-it";
import MarkdownItAttrs from "markdown-it-attrs";
import defaultsDeep from "lodash.defaultsdeep";
import { default as _debounce } from "lodash.debounce";
/**
* @callback waitForElCb
* @param {HTMLElement[]} queries
*/
/**
*
* @param {String[]} els
* @param {waitForElCb} cb
* @param {Number} [tries=100]
* @param {Number} [interval=300]
*/
export function waitForElement(els, cb, tries = 100, interval = 300) {
const queries = els.map((el) => document.querySelector(el));
if (queries.every((a) => a)) {
cb(queries);
} else if (tries > 0) {
setTimeout(waitForElement, interval, els, cb, --tries);
}
}
export 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;
}
export function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function getClosestToNum(arr, num) {
return arr.reduce((prev, curr) => (Math.abs(curr - num) < Math.abs(prev - num) ? curr : prev));
}
export function renderMD(src, env) {
const md = MarkdownIt("commonmark", {
html: true,
breaks: true,
linkify: true,
typographer: true
});
md.use(MarkdownItAttrs);
return md.render(src, env);
}
export function htmlToNode(htmlStr) {
var div = document.createElement("div");
div.innerHTML = htmlStr.trim();
return div.firstChild;
}
export function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
export function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* @template T
* @param {T[]} arr
* @returns {T}
*/
export function randomFromArray(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
/** @type {_debounce} */
export function debounce(fn, wait, opts) {
return _debounce(fn, wait, opts);
}
/**
* @param {Object} options
* @param {...Object} defaults
* @returns {Object}
*/
export function defaults(options, ...defaults) {
return defaultsDeep(options, ...defaults);
}

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ $colors: (
subtext: #f0f0f0,
sidebar-text: #ffffff,
main: #000000,
sidebar: #1ed760,
sidebar: #121212,
player: #000000,
card: #000000,
shadow: #202020,
@ -17,14 +17,6 @@ $colors: (
misc: #bfbfbf
);
// Set initial Vars
:root {
@each $key, $color in $colors {
--spice-#{$key}: #{$color};
--spice-#{$key}-rgb: #{red($color)}, #{green($color)}, #{blue($color)};
}
}
// Set Transitions
@each $key, $color in $colors {
@property --spice-#{$key} {
@ -33,7 +25,7 @@ $colors: (
inherits: true;
}
@property --spice-#{$key}-rgb {
@property --spice-rgb-#{$key} {
syntax: "<number>, <number>, <number>";
initial-value: #{red($color)}, #{green($color)}, #{blue($color)};
inherits: true;
@ -46,18 +38,9 @@ $props-to-transition: ("sidebar", "main", "text", "button");
@each $key in $props-to-transition {
$props: append($props, --spice-#{$key}, comma);
$props: append($props, --spice-#{$key}-rgb, comma);
$props: append($props, --spice-rgb-#{$key}, comma);
}
transition: all var(--song-transition-speed) linear;
transition-property: $props;
}
// Color Function
@function spiceColor($key, $alpha: 1) {
@if $alpha == 1 {
@return var(--spice-#{$key});
} @else {
@return rgba(var(--spice-#{$key}-rgb), $alpha);
}
}

View file

@ -9,50 +9,76 @@
&[active] {
display: flex;
& ~ #main::after {
z-index: 4;
position: absolute;
content: "";
inset: 0px;
backdrop-filter: blur(3px) brightness(60%);
}
}
.dribbblish-config-container {
z-index: 1;
position: relative;
width: clamp(500px, 50%, 650px);
background-color: spiceColor("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;
gap: 8px;
flex-direction: column;
align-items: center;
justify-content: center;
@include spiceGlass();
> h2 {
font-size: 32px;
line-height: 32px;
}
.dribbblish-config-close {
position: absolute;
top: 15px;
right: 15px;
padding: 0px;
top: 24px;
right: 24px;
}
.dribbblish-config-search {
// TODO: improve styles
}
.dribbblish-config-areas {
display: flex;
width: 100%;
flex-direction: column;
gap: 16px;
gap: 8px;
max-height: 60vh;
overflow-y: auto;
padding: 0px 50px;
padding: 0px 26px;
.dribbblish-config-area {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
gap: 8px;
&[collapsed] {
&[collapsed]:not([search]) {
overflow: hidden;
min-height: 38px; //for some reason height alone isn't enough
height: 38px;
> h2 svg {
transform: rotate(270deg);
.dribbblish-config-area-header {
svg {
transform: rotate(0deg);
}
&:hover svg {
transform: rotate(0deg) scale(1.1);
}
&:active svg {
transform: rotate(0deg) scale(0.9);
}
}
}
@ -63,6 +89,7 @@
.dribbblish-config-area-header {
position: relative;
text-align: center;
width: fit-content;
height: 38px;
display: flex;
gap: 10px;
@ -74,65 +101,146 @@
padding: 0px;
height: 100%;
stroke-width: 2px;
transform: rotate(90deg);
transform: rotate(180deg);
}
&:hover svg {
transform: rotate(180deg) scale(1.1);
}
&:active svg {
transform: rotate(180deg) scale(0.9);
}
}
.dribbblish-config-area-items {
display: flex;
flex-direction: column;
gap: 16px;
gap: 8px;
width: 100%;
.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";
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 8px 16px;
user-select: text;
&[parent] {
padding-left: 16px;
}
&[hidden="true"] {
&[hidden],
&[hidden-override] {
display: none;
}
> {
.x-settings-title {
display: flex;
gap: 10px;
align-items: center;
grid-area: header;
margin: 0px;
height: min-content;
position: relative;
&::before {
z-index: -1;
content: "";
position: absolute;
inset: 0px;
border-radius: var(--main-corner-radius);
background-color: spiceColor("subtext", 0.03, 0.04);
}
&[parent]::before {
top: -8px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
&[connect-bottom]::before {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
&[invalid] {
&::before {
border: 2px solid rgba(red, 0.8);
}
.dribbblish-config-item-input::before {
content: var(--validation-error);
margin-right: 8px;
color: rgba(red, 0.8);
}
}
&[changed] {
&::after {
content: "";
position: absolute;
left: 0px;
top: 0px;
bottom: 0px;
width: 5px;
background-color: spiceColor("text");
border-top-left-radius: var(--main-corner-radius);
border-bottom-left-radius: var(--main-corner-radius);
}
&.no-desc {
bottom: -10px;
&[parent]::after {
top: -4px;
border-top-left-radius: 0px;
}
&[connect-bottom]::after {
bottom: -4px;
border-bottom-left-radius: 0px;
}
.dribbblish-config-item-reset {
display: block !important;
}
}
.dribbblish-config-item-header {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
> {
& [empty="true"] {
display: none;
}
.dribbblish-config-item-reset {
padding: 0px;
color: spiceColor("text");
width: 1em;
height: 1em;
.x-settings-title {
display: flex;
gap: 5px;
align-items: center;
grid-area: header;
margin: 0px;
height: min-content;
position: relative;
bottom: 0px;
.dribbblish-config-item-reset {
display: none;
width: 20px;
height: 20px;
padding: 0px;
color: spiceColor("text");
}
}
.main-type-mesto {
grid-area: description;
height: min-content;
color: spiceColor("subtext");
line-height: calc(1em + 6px); // To have line gaps
}
.x-settings-secondColumn {
grid-area: input;
}
}
}
.main-type-mesto {
grid-area: description;
height: min-content;
color: spiceColor("subtext");
}
.x-settings-secondColumn {
grid-area: input;
}
.dribbblish-config-item-input {
display: flex;
min-width: fit-content;
}
}
}
@ -144,6 +252,5 @@
position: absolute;
content: "";
inset: 0px;
backdrop-filter: blur(3px) brightness(60%);
}
}

View file

@ -0,0 +1,41 @@
.connect-device-list-container {
padding: 8px;
@include spiceGlass(lightOffset(0.8, -0.1));
&::before {
display: none;
}
.connect-device-list-content {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
.connect-header {
padding: 0px;
filter: invert(var(--is_light));
}
.connect-device-list {
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
.connect-device-list-item {
background-color: transparent;
border-radius: var(--main-corner-radius);
&:hover,
&:active {
background-color: spiceColor("subtext", 0.1, 0.1);
}
&--active {
background-color: spiceColor("selected-row", 0.2) !important;
}
}
}
}
}

View file

@ -1,7 +1,7 @@
.main-contextMenu-menu {
background-color: transparent !important;
overflow: visible;
position: relative;
overflow: visible;
background-color: transparent !important;
border-radius: var(--main-corner-radius);
padding: 8px;
@ -10,9 +10,7 @@
content: "";
position: absolute;
inset: 0px;
background-color: spiceColor("main", calc(0.75 - 0.1 * var(--is_light))) !important;
backdrop-filter: blur(10px);
border-radius: var(--main-corner-radius);
@include spiceGlass(lightOffset(0.8, -0.1));
}
.main-contextMenu-menuItem {
@ -21,16 +19,16 @@
color: spiceColor("subtext") !important;
&:hover,
&:focus,
&:active,
&[aria-expanded="true"] {
background-color: spiceColor("subtext", calc(0.1 + 0.3 * var(--is_light)));
background-color: spiceColor("subtext", 0.1, 0.3);
}
&::before,
&::after {
left: 5px;
right: 5px;
left: 8px;
right: 8px;
border-color: spiceColor("subtext", 0.1, 0.1);
}
}
}

View file

@ -0,0 +1,32 @@
.main-topBar-topbarContent {
height: 100%;
& > nav[class*="-tabBar"][class*="-tabBar-nav"] {
height: 100%;
& > ul {
display: flex;
gap: 8px;
height: 100%;
li[class*="-tabBar-headerItem"] {
height: 100%;
& > a {
display: flex;
align-items: center;
justify-content: center;
margin: 0px;
height: 100%;
padding: 0px 8px;
border-radius: var(--sidebar-icons-border-radius);
& > span {
line-height: 13px;
@include spiceFont("glue", 14px, "Medium");
}
}
}
}
}
}

57
src/styles/Fonts.scss Normal file
View file

@ -0,0 +1,57 @@
// GoogleSans: https://www.cufonfonts.com/font/google-sans
// Roboto: https://fonts.google.com/specimen/Roboto
$font-names: (
"glue": "GoogleSans",
"info": "Roboto"
);
$font-weights: (
"Thin": 100,
"Light": 300,
"Regular": 400,
"Medium": 500,
"Bold": 700,
"Black": 900
);
// add @font-face rules
@each $prefix, $font in $font-names {
@each $style, $weight in $font-weights {
@font-face {
font-family: $font;
font-weight: $weight;
font-style: normal;
src: url(font64("#{$font}-#{$style}.ttf")) format("truetype");
}
}
}
// spiceFont mixin
@mixin spiceFont($type: "glue", $size: null, $weight: null) {
@if map-has-key($font-names, $type) {
font-family: var(--#{$type}-font-family);
} @else {
@error "$type is invalid";
}
@if $size != null {
font-size: $size;
}
@if $weight != null {
@if type-of($weight) == "number" and unit($weight) == "" {
font-weight: $weight;
} @else if map-has-key($font-weights, $weight) {
font-weight: #{map-get($font-weights, $weight)};
} @else {
@error "$weight is invalid";
}
}
}
// set font variables
:root {
--glue-font-family: #{map-get($font-names, "glue")}, #{map-get($font-names, "info")}, spotify-circular, spotify-circular-cyrillic, spotify-circular-arabic, spotify-circular-hebrew, Helvetica Neue, helvetica, arial, Hiragino Kaku Gothic Pro, Meiryo, MS Gothic, sans-serif;
--info-font-family: #{map-get($font-names, "info")}, spotify-circular, spotify-circular-cyrillic, spotify-circular-arabic, spotify-circular-hebrew, Helvetica Neue, helvetica, arial, Hiragino Kaku Gothic Pro, Meiryo, MS Gothic, sans-serif;
font-family: var(--glue-font-family);
letter-spacing: normal;
}

7
src/styles/Icons.scss Normal file
View file

@ -0,0 +1,7 @@
svg[icon-type="dribbblish"] {
overflow: visible;
// &[icon-style="custom"] {}
// &[icon-style^="material:"] {}
}

32
src/styles/Info.scss Normal file
View file

@ -0,0 +1,32 @@
#dribbblish-info-container {
display: flex;
gap: 8px;
height: 100%;
&:empty {
display: none;
}
& > div {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 100%;
padding: 0px 8px;
color: spiceColor("sidebar-text");
background-color: spiceColor("sidebar");
border-radius: var(--sidebar-icons-border-radius);
line-height: 13px;
@include spiceFont("glue", 14px, "Medium");
&[clickable] {
cursor: pointer;
}
&[icon-only] {
padding: 0px;
aspect-ratio: 1/1;
}
}
}

View file

@ -1,26 +1,109 @@
button.main-button-primary {
background-color: spiceColor("selected-row", 0.4) !important;
.main-button {
&-primary {
$color: spiceColor("subtext");
color: $color;
background-color: spiceColor("selected-row", 0.4) !important;
&:hover,
&:active {
background-color: spiceColor("selected-row", 0.6) !important;
&:hover,
&:active {
background-color: spiceColor("selected-row", 0.6) !important;
}
span {
color: $color !important;
}
}
span {
color: spiceColor("subtext") !important;
// ? Don't know if this exists
// &-secondary {
// $color: spiceColor("subtext");
// color: $color;
// background-color: spiceColor("selected-row", 0.4) !important;
// &:hover,
// &:active {
// background-color: spiceColor("selected-row", 0.6) !important;
// }
// span {
// color: $color !important;
// }
// }
// ? the `:not(...)` is to fix #137
&-tertiary:not(.main-entityHeader-titleButton) {
$color: spiceColor("subtext");
color: $color;
background-color: spiceColor("selected-row", 0.2, 0.05) !important;
&:hover,
&:active {
background-color: spiceColor("selected-row", 0.6) !important;
}
span {
color: $color !important;
}
}
}
input,
textarea {
// Checkbox
.x-toggle-indicatorWrapper {
background-color: spiceColor("subtext", 0.1);
input:not(:disabled):hover ~ & {
background-color: spiceColor("subtext", 0.15) !important;
}
input:checked ~ & {
background-color: spiceColor("selected-row", 0.4) !important;
}
input:not(:disabled):hover:checked ~ & {
background-color: spiceColor("selected-row", 0.6) !important;
}
}
textarea,
select,
input {
background-color: spiceColor("selected-row", 0.4) !important;
border-radius: 4px !important;
padding: 6px 10px 6px 48px;
color: spiceColor("text") !important;
color: spiceColor("subtext") !important;
outline: none;
border: none;
&:hover,
&:active {
background-color: spiceColor("selected-row", 0.6) !important;
&:not(:disabled) {
&:hover,
&:active {
background-color: spiceColor("selected-row", 0.6) !important;
}
}
&:disabled {
cursor: not-allowed;
opacity: 0.5 !important;
}
&::placeholder {
color: spiceColor("subtext");
opacity: lightOffset(0.8, 0.1);
}
}
textarea {
padding: 6px 10px;
}
select > option {
background: spiceColor("main") !important;
}
input {
padding: 6px 10px 6px 48px;
&[type="checkbox"] {
opacity: 0 !important;
}
&[type="range"] {
@ -42,30 +125,28 @@ textarea {
}
}
&::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: spiceColor("sidebar-text");
background-color: spiceColor("button");
transition: opacity 0.25s ease;
opacity: 0;
}
&[tooltip] {
&::after {
z-index: 9999;
content: attr(tooltip);
position: absolute;
min-width: 50px;
top: -22px;
left: 50%;
transform: translateX(calc(-50%));
padding: 0 5px;
border-radius: 4px;
text-align: center;
color: spiceColor("sidebar-text");
background-color: spiceColor("button");
transition: opacity 0.25s ease;
opacity: 0;
}
&:hover::after,
&:active::after {
opacity: 1;
}
&:focus {
outline: none;
&:hover::after,
&:active::after {
opacity: 1;
}
}
&::-webkit-slider-runnable-track {
@ -78,37 +159,21 @@ textarea {
&[type="number"],
&[type="text"],
&[type="search"],
&[type="time"] {
height: 32px;
border: none;
border-radius: 4px !important;
padding: 0px 10px;
color: spiceColor("subtext") !important;
}
&[type="time"] {
&::-webkit-calendar-picker-indicator {
filter: invert(calc(1 - var(--is_light)));
filter: invert(var(--is_dark));
}
}
&[type="color"] {
position: relative;
padding: 0px;
border: none;
&::before {
z-index: -1;
content: "";
position: absolute;
inset: -5px;
border-radius: 4px;
background-color: spiceColor("selected-row", 0.4);
}
&:hover::before,
&:active::before {
background-color: spiceColor("selected-row", 0.6);
}
padding: 1px 3px;
}
}

1
src/styles/Lyrics.scss Normal file
View file

@ -0,0 +1 @@
// TODO: improve default lyrics styles when the classes are mapped `see: https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/144`

21
src/styles/Markdown.scss Normal file
View file

@ -0,0 +1,21 @@
*[markdown] {
ol,
ul {
list-style: inside;
> li::marker {
content: "";
font-weight: 900;
}
}
code {
background-color: spiceColor("subtext", 0.1);
padding: 0px 5px;
border-radius: 3px;
}
.muted {
color: spiceColor("subtext", 0.5, 0.2);
}
}

67
src/styles/Modals.scss Normal file
View file

@ -0,0 +1,67 @@
.GenericModal__overlay {
backdrop-filter: blur(3px) brightness(60%);
background-color: transparent;
.GenericModal {
z-index: 1;
position: relative;
padding: 24px 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
@include spiceGlass();
// Popups (`Spicetify.PopupModal.display()`)
generic-modal & {
color: spiceColor("text");
width: clamp(500px, 50%, 650px);
}
// Dialogs (Delete Folder)
.ReactModalPortal > & {
> * {
color: spiceColor("subtext");
background-color: transparent;
padding: 0px;
}
}
.main- {
&trackCreditsModal,
&embedWidgetGenerator {
&-container {
display: flex;
gap: 16px;
background-color: transparent;
width: 100%;
box-shadow: none;
}
&-header {
display: flex;
gap: 16px;
padding: 0px;
width: 100%;
border: none;
h1 {
text-align: center;
width: 100%;
@include spiceFont("glue", 2em, "Bold");
}
button {
position: absolute;
top: 16px;
right: 16px;
}
}
&-mainSection {
padding: 0px 26px;
}
}
}
}
}

View file

@ -1,17 +1,31 @@
#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 */
// Hide ads
#main[hide-ads~="ads"] {
// Remove ad placeholder in main screen
.main-leaderboardComponent-container {
display: none;
}
}
// Hide premium features
#main[hide-ads~="premium"] {
// Remove upgrade button
.main-topBar-UpgradeButton {
display: none;
}
// Remove upgrade to premium button in user menu
// prettier-ignore
.main-contextMenu-menuItemButton[href="https://www.spotify.com/premium/"] {
display: none;
}
// Hide fullscreen button
.volume-bar + .control-button {
display: none;
}
// Hide download buttons
.main-actionBar-ActionBarRow button[role="switch"] {
display: none;
}
}

42
src/styles/Overlay.scss Normal file
View file

@ -0,0 +1,42 @@
#dribbblish-overlay {
z-index: 999999;
position: fixed;
inset: 0px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
opacity: 0;
transition: opacity 1s ease-in;
color: spiceColor("subtext");
@include spiceFont("glue", 32px, "Bold");
&[active] {
opacity: 1;
pointer-events: all;
}
&::before {
z-index: -1;
content: "";
position: absolute;
inset: 0px;
background-color: spiceColor("main", 0.9, -0.1);
backdrop-filter: blur(10px);
}
& > div {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
& > * {
color: var(--color);
&:empty {
display: none;
}
}
}
}

16
src/styles/Util.scss Normal file
View file

@ -0,0 +1,16 @@
// SASS overwrites the CSS invert function so we overwrite it back
@function invert($v) {
@return #{"invert("}$v#{")"};
}
@mixin spiceShadow() {
box-shadow: 0px 0px 8px spiceColor("subtext", 0.1, 0.1);
}
@mixin spiceGlass($opacity: 0.95) {
background-color: spiceColor("main", $opacity);
backdrop-filter: blur(10px);
border: 1px solid spiceColor("subtext", 0.1, 0.1);
border-radius: var(--main-corner-radius);
@include spiceShadow();
}

View file

@ -1,13 +1,18 @@
// SASS overwrites the CSS invert function so we overwrite it back
@function invert($v) {
@return #{"invert("}$v#{")"};
}
@import "Util";
@import "Colors";
@import "Fonts";
@import "Inputs";
@import "ConfigMenu";
@import "ContextMenu.scss";
@import "ContextMenu";
@import "NoAds";
@import "Markdown";
@import "Info";
@import "Modals";
@import "ConnectDeviceList";
@import "Lyrics";
@import "Icons";
@import "Overlay";
@import "CustomAppTabBar";
:root {
--bar-height: 70px;
@ -17,66 +22,46 @@
--scrollbar-vertical-size: 8px;
--cover-border-radius: 8px;
--playbar-movement-anim-speed: 0.5s;
--image-radius: 10px;
--image-radius: 4px;
--sidebar-icons-border-radius: 50vh; // 50vh = round / pill
--song-transition-speed: 3s;
}
--is_dark: calc(1 - var(--is_light));
@font-face {
font-family: "Google Sans Display";
src: url("glue-resources/fonts/GoogleSansDisplayRegular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Google Sans Display";
src: url("glue-resources/fonts/GoogleSansDisplayMedium.woff2") format("woff2");
font-style: normal;
font-weight: 500;
}
@font-face {
font-family: "Roboto";
src: url("glue-resources/fonts/Roboto.woff2") format("woff2");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Roboto";
src: url("glue-resources/fonts/RobotoMedium.woff2") format("woff2");
font-style: normal;
font-weight: 500;
}
body {
--glue-font-family: "Google Sans Display", "Roboto", spotify-circular, spotify-circular-cyrillic, spotify-circular-arabic, spotify-circular-hebrew, Helvetica Neue, helvetica, arial, Hiragino Kaku Gothic Pro, Meiryo, MS Gothic, sans-serif;
--info-font-family: "Roboto", spotify-circular, spotify-circular-cyrillic, spotify-circular-arabic, spotify-circular-hebrew, Helvetica Neue, helvetica, arial, Hiragino Kaku Gothic Pro, Meiryo, MS Gothic, sans-serif;
font-family: var(--glue-font-family);
letter-spacing: normal;
}
.os-scrollbar-handle {
background-color: spiceColor("text") !important;
border-radius: calc(var(--scrollbar-vertical-size) / 2);
}
.os-scrollbar-handle:hover {
filter: brightness(80%);
// Display warning for incorrectly installed js
@keyframes noJsShow {
to {
opacity: 1;
pointer-events: all;
}
}
&:not([dribbblish-js-installed])::after {
content: "dribbblish-dynamic.js not installed correctly";
position: fixed;
inset: 0px;
color: red;
background-color: black;
text-align: center;
line-height: 100vh;
opacity: 0;
pointer-events: none;
animation: noJsShow 0s ease-in 3s forwards;
@include spiceFont("glue", 32px, "Bold");
}
}
::-webkit-scrollbar {
width: var(--scrollbar-vertical-size);
}
.os-scrollbar-handle,
::-webkit-scrollbar-thumb {
background-color: spiceColor("text") !important;
border-radius: calc(var(--scrollbar-vertical-size) / 2);
background-color: spiceColor("sidebar") !important;
border-radius: 50vw;
}
.os-scrollbar-handle:hover,
::-webkit-scrollbar-thumb:hover {
filter: brightness(80%);
background-color: spiceColor("sidebar", 0.8) !important;
}
.main-type-mesto,
@ -84,7 +69,7 @@ body {
.main-type-ballad,
.main-type-balladBold,
.main-type-canon {
font-family: var(--info-font-family);
@include spiceFont("info");
letter-spacing: normal;
}
@ -93,7 +78,7 @@ body {
}
.lyrics-lyricsContainer-LyricsLine {
font-family: var(--glue-font-family);
@include spiceFont("glue");
letter-spacing: -0.03em !important;
}
@ -106,6 +91,24 @@ body {
color: spiceColor("text") !important;
}
#dribbblish-search-box {
display: none;
order: 99;
height: 28px;
border-radius: var(--sidebar-icons-border-radius) !important;
@include spiceFont("info", 14px, "Regular");
}
#main[search-box] {
#dribbblish-search-box {
display: block;
}
.main-topBar-topbarContent form[role="search"] {
display: none;
}
}
.main-home-homeHeader,
.x-entityHeader-overlay,
.x-actionBarBackground-background,
@ -120,15 +123,6 @@ body {
color: white;
}
.connect-title,
.connect-header {
display: none;
}
.connect-device-list {
margin: 0px -5px;
}
/* Remove Topbar background colour */
.main-topBar-background {
background-color: unset !important;
@ -137,18 +131,31 @@ body {
background-color: spiceColor("main");
}
.main-entityHeader-shadow,
.connect-device-list-container {
.main-entityHeader-shadow {
box-shadow: 0 4px 20px #21212130;
}
.main-trackList-rowMarker {
color: spiceColor("text");
}
.main-trackList-playingIcon {
-webkit-mask-image: url(icon64("equaliser", '{"size": 14}'));
background-color: currentColor;
content-visibility: hidden;
image-rendering: pixelated;
filter: grayscale(1);
}
.main-trackList-trackListRowGrid {
background-color: var(--spice-main);
}
.main-trackList-active {
background-color: spiceColor("selected-row", 0.25) !important;
}
.main-trackList-trackListRow:hover {
background-color: spiceColor("selected-row", 0.2) !important;
background-color: spiceColor("selected-row", 0.15) !important;
}
.main-trackList-trackListRow:focus-within,
@ -163,6 +170,11 @@ span.artist-artistVerifiedBadge-badge svg > path:last-of-type {
fill: spiceColor("text");
}
/* Playlist text color */
.main-entityHeader-subtitle.main-entityHeader-gra {
color: spiceColor("subtext");
}
/* Full window artist background */
.main-entityHeader-background.main-entityHeader-gradient {
opacity: 0.3;
@ -306,17 +318,8 @@ span.artist-artistVerifiedBadge-badge svg > path:last-of-type {
.main-userWidget-box {
background-color: transparent !important;
}
.main-userWidget-box.update-avail {
padding-right: 10px;
background-color: spiceColor("subtext", 0.1) !important;
&:hover,
&:active,
&[data-context-menu-open="true"] {
background-color: spiceColor("subtext", 0.15) !important;
}
padding: 0px;
margin: 0px;
}
.main-avatar-avatar.main-avatar-withBadge:after {
@ -331,6 +334,7 @@ span.artist-artistVerifiedBadge-badge svg > path:last-of-type {
}
.main-nowPlayingBar-container {
position: relative;
border-radius: 0 0 var(--main-corner-radius) var(--main-corner-radius);
background-color: spiceColor("main");
border-top: 0;
@ -346,11 +350,27 @@ span.artist-artistVerifiedBadge-badge svg > path:last-of-type {
}
.main-connectBar-connectBar {
border-radius: 0 0 var(--main-corner-radius) var(--main-corner-radius);
border: 2px solid spiceColor("main");
border-top: 0;
background-color: spiceColor("button") !important;
color: spiceColor("text") !important;
// These styles are useless, but maybe we'll change this back to the way it was before so I'll leave them
display: none;
// position: absolute;
// display: flex;
// flex-direction: row-reverse;
// gap: 8px;
// height: fit-content;
// right: 4px;
// bottom: 4px;
// padding: 2px 8px;
// background-color: spiceColor("text", 0.2) !important;
// border-radius: var(--main-corner-radius);
// &::after {
// display: none;
// }
// svg {
// margin: 0px;
// }
}
.Root__nav-bar {
@ -408,8 +428,9 @@ html {
.main-rootlist-statusIcons {
z-index: 1;
inset: 0;
position: absolute;
inset: 0;
left: calc(var(--indentation) * var(--left-sidebar-item-indentation-width));
margin-left: 12px;
display: flex;
align-items: center;
@ -420,6 +441,10 @@ html {
display: none;
}
html.sidebar-hide-text & {
left: 0px;
}
.main-playButton-button {
width: 32px;
height: 32px;
@ -508,29 +533,17 @@ html.sidebar-hide-text .GlueDropTarget span {
display: none;
}
.x-toggle-indicatorWrapper {
background-color: transparent;
backdrop-filter: invert(0.25);
}
.main-topBar-historyButtons {
display: flex;
align-items: center;
gap: 8px;
input:checked ~ .x-toggle-indicatorWrapper {
background-color: spiceColor("button", 0.4);
}
input:hover:checked ~ .x-toggle-indicatorWrapper {
background-color: spiceColor("button", 0.5) !important;
}
input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
background-color: transparent;
backdrop-filter: invert(0.4);
}
/** */
.main-topBar-historyButtons .main-topBar-button {
background-color: unset;
width: 24px;
height: 24px;
.main-topBar-button {
margin: 0px;
background-color: unset;
width: 24px;
height: 24px;
}
}
.main-topBar-historyButtons svg {
@ -546,13 +559,9 @@ input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
.playback-bar {
position: absolute;
width: var(--main-view-width);
left: var(--sidebar-width);
bottom: calc(var(--main-gap) + var(--bar-height) - 12px / 2);
}
.Root.is-connectBarVisible .playback-bar {
bottom: calc(var(--main-gap) + var(--bar-height) + 24px - 12px / 2);
left: 0px;
right: 0px;
top: calc(0px - 12px / 2);
}
.main-nowPlayingWidget-coverArt .cover-art {
@ -567,6 +576,7 @@ input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
.progress-bar {
--progress-bar-height: 2px;
--is-active-fg-color: #{spiceColor("button-active")};
--fg-color: #{spiceColor("button")};
--bg-color: #{spiceColor("text", 0.2)};
}
@ -582,12 +592,18 @@ input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
opacity: 1;
}
.progress-bar:not(:active) .x-progressBar-progressBarBg > div:first-child > div {
transition: transform var(--playbar-movement-anim-speed) ease;
}
#main[playbar-transition] {
.progress-bar:not(:active) .x-progressBar-progressBarBg > div:first-child > div {
transition: transform var(--playbar-movement-anim-speed) ease;
}
.progress-bar:not(:active) .progress-bar__slider {
transition-property: left, opacity;
.progress-bar:not(:active) .progress-bar__slider {
transition-property: left, opacity;
}
.playback-bar:not(:active) .prog-tooltip {
transition: transform var(--playbar-movement-anim-speed) ease;
}
}
.playback-bar .prog-tooltip {
@ -597,15 +613,11 @@ input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
left: 50%;
transform: translateX(calc(-50%));
padding: 0 5px;
border-radius: 4px;
text-align: center;
color: spiceColor("text");
background-color: spiceColor("button");
white-space: nowrap;
pointer-events: none;
}
.playback-bar:not(:active) .prog-tooltip {
transition: transform var(--playbar-movement-anim-speed) ease;
color: spiceColor("subtext");
@include spiceGlass();
}
.minimal-player .player-controls__buttons {
@ -654,12 +666,23 @@ input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
color: spiceColor("subtext");
&:hover,
&:active,
&:focus {
&:active {
color: spiceColor("text");
}
}
#main-trackInfo-year {
#main[playbar-album-info="false"] & {
display: none;
}
}
#main-trackInfo-genre {
#main[playbar-genre-info="false"] & {
display: none;
}
}
.main-topBar-topbarContent .main-playButton-PlayButton {
--size: 35px !important;
}
@ -690,7 +713,7 @@ input:hover:not([disabled]):not(:active) ~ .x-toggle-indicatorWrapper {
padding: 0 12px;
}
img.playlist-picture {
.playlist-picture {
width: 32px;
height: 32px;
flex: 0 0 32px;
@ -698,6 +721,10 @@ img.playlist-picture {
background-size: cover;
background-position: center;
border-radius: var(--sidebar-icons-border-radius);
&[type="icon"] {
padding: 2px;
}
}
.main-rootlist-rootlistItem a span {
@ -789,12 +816,15 @@ li.GlueDropTarget {
padding-top: 32px;
}
#main[top-bar="none-padding"] .spotify__container--is-desktop.spotify__os--is-windows[dir="ltr"] .Root__top-bar + .main-buddyFeed-buddyFeedRoot .main-topBar-container {
padding-right: 167px;
}
.main-topBar-container {
display: flex;
gap: 8px;
max-width: unset;
padding: 16px 32px !important;
& > * {
height: 100%;
}
}
/** Custom elements */
@ -812,18 +842,6 @@ li.GlueDropTarget {
pointer-events: none;
}
#dribbblish-update {
color: spiceColor("button-active");
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 400;
letter-spacing: normal;
line-height: 16px;
text-transform: none;
}
/** Rearrange player bar */
.main-nowPlayingBar-left {
order: 1;
@ -861,7 +879,6 @@ li.GlueDropTarget {
.main-nowPlayingWidget-nowPlaying {
justify-content: center;
// Workaround for #73 (https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/73)
& button {
color: spiceColor("subtext");
@ -926,9 +943,9 @@ li.GlueDropTarget {
.main-collectionLinkButton-collectionLinkText,
.main-createPlaylistButton-text,
.main-navBar-navBarLink > span {
font-size: 14px;
font-weight: 400;
.main-navBar-navBarLink > span,
.main-rootlist-rootlistItemLink > span {
@include spiceFont("info", 14px, "Regular");
letter-spacing: normal;
line-height: 20px;
text-transform: none;
@ -1033,7 +1050,9 @@ span.main-userWidget-displayName,
}
.main-rootlist-wrapper > div:nth-child(2) > li img,
.main-navBar-navBarLink > .icon {
.main-navBar-navBarLink > svg,
.main-navBar-navBarLink > .icon,
.main-rootlist-rootlistItemLink > svg {
z-index: 1;
}
@ -1053,7 +1072,7 @@ span.main-userWidget-displayName,
left: -200%;
opacity: 0.4;
background-color: black;
border-radius: var(--image-radius);
border-radius: var(--sidebar-icons-border-radius);
pointer-events: none;
transition: all calc(var(--sidebar-icons-hover-animation) * 0.2s) ease;
transition-property: left, opacity;
@ -1110,17 +1129,13 @@ div.GlueDropTarget.personal-library > *.active {
left: calc(var(--sidebar-width) + 10px);
}
.Root.is-connectBarVisible .main-coverSlotExpanded-container {
bottom: calc(var(--main-gap) + var(--bar-height) + 24px + 10px);
}
html.right-expanded-cover .main-coverSlotExpanded-container {
right: calc(var(--main-gap) + 10px);
right: calc(var(--main-gap) * 2);
left: unset;
}
html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
right: calc(var(--main-gap) + var(--buddy-feed-width) + 10px);
right: calc(var(--main-gap) + var(--buddy-feed-width));
left: unset;
}
@ -1172,22 +1187,6 @@ html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
border-radius: 5px;
}
.main-dropDown-dropDown,
.x-sortBox-sortDropdown {
background-color: spiceColor("selected-row", 0.4) !important;
color: spiceColor("subtext");
}
.main-dropDown-dropDown:hover,
.x-sortBox-sortDropdown:hover {
background-color: spiceColor("selected-row", 0.6) !important;
}
.connect-device-list-item:focus,
.connect-device-list-item:hover {
background-color: spiceColor("selected-row", 0.3);
}
/* 1.1.56 */
.main-navBar-navBar {
width: var(--sidebar-width) !important;
@ -1233,10 +1232,6 @@ html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
.main-tag-container {
background-color: spiceColor("text");
}
/* progressbar tooltip text color */
.playback-bar .prog-tooltip {
color: spiceColor("sidebar-text") !important;
}
/* edit button of CustomApps */
.reddit-sort-container button.switch,
@ -1273,6 +1268,10 @@ html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
color: spiceColor("sidebar-text") !important;
}
.main-shelf-title {
@include spiceFont("glue");
}
/* translucent background cover */
.Root__top-container::before {
z-index: 3;
@ -1286,18 +1285,68 @@ html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
background-position: center center;
filter: blur(15px);
border-radius: var(--main-corner-radius);
opacity: calc(0.07 + 0.03 * var(--is_light, 0));
opacity: lightOffset(0.07, 0.03);
transition: background-image var(--song-transition-speed) linear;
html:not(.buddyfeed-visible) & {
inset: var(--main-gap) 0px var(--main-gap) var(--sidebar-width);
}
}
// Fix lyrics-plus having a scrollbar when top-bar is `solid` or `transparent`
.lyrics-lyricsContainer-LyricsContainer {
height: 100% !important;
}
// Fix main-view-containers having a scrollbar when top-bar is `solid` or `transparent`
.main-view-container__scroll-node-child {
height: 100%;
padding-bottom: 0px;
& > * {
height: 100% !important;
}
}
// Hide default Spotify "Offline" notice
.main-noConnection {
display: none;
}
// Notifications
.main-notificationBubbleContainer-NotificationBubbleContainer {
.main-notificationBubble-NotificationBubble {
color: spiceColor("subtext");
@include spiceGlass();
}
}
// ! WORKAROUNDS / TEMP FIXES
// Spotify UI breaks after advertisements #63
canvas[width="250"][height="250"] {
display: none;
}
// workaround to remove gradient near album info #103
.main-trackInfo-container > div > div:before,
.main-trackInfo-container > div > div:after {
display: none;
}
// workaround to re-center track-info
#main:not([player-controls="spotify"]) .main-trackInfo-container > div > div > div {
display: flex;
justify-content: center;
}
// fix albumn name font size (#171)
.main-trackInfo-release {
font-size: 10px;
}
// fix play buttons color going green (#174)
.encore-bright-accent-set {
--background-highlight: var(--spice-button) !important;
--background-press: var(--spice-button-active) !important;
color: var(--spice-sidebar-text);
}
// hide the triangle under the connect button
.control-button--active::after {
display: none;
}

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="currentColor" d="M255.545 8c-66.269.119-126.438 26.233-170.86 68.685L48.971 40.971C33.851 25.851 8 36.559 8 57.941V192c0 13.255 10.745 24 24 24h134.059c21.382 0 32.09-25.851 16.971-40.971l-41.75-41.75c30.864-28.899 70.801-44.907 113.23-45.273 92.398-.798 170.283 73.977 169.484 169.442C423.236 348.009 349.816 424 256 424c-41.127 0-79.997-14.678-110.63-41.556-4.743-4.161-11.906-3.908-16.368.553L89.34 422.659c-4.872 4.872-4.631 12.815.482 17.433C133.798 479.813 192.074 504 256 504c136.966 0 247.999-111.033 248-247.998C504.001 119.193 392.354 7.755 255.545 8z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 656 B

View file

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

View file

@ -5,8 +5,8 @@
set -e
echo "UN-INSTALLING"
cd "$(dirname "$(spicetify -c)")/Themes/DribbblishDynamic"
spicetify config current_theme " " extensions dribbblish-dynamic.js-
cd "$(dirname "$(spicetify -c)")"
spicetify config current_theme "SpicetifyDefault" extensions dribbblish-dynamic.js-
echo "UN-PATCHING"
if cat config-xpui.ini | grep -o '\[Patch\]'; then

110
webpack.config.babel.js Normal file
View file

@ -0,0 +1,110 @@
import webpack from "webpack";
import sass from "sass";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
import path from "path";
import fs from "fs";
import Icons from "./src/js/Icons";
import { lightOffset, spiceColor } from "./src/js/SassUtil";
const _icons = {};
function addIcon(name, style, path) {
name = name.replace(/_/g, "-");
if (!_icons.hasOwnProperty(name)) _icons[name] = {};
_icons[name][style] = fs.readFileSync(path, { encoding: "utf8" });
}
// Add Material Icons
let iconDir = path.resolve(__dirname, "node_modules/@material-icons/svg/svg");
for (const dir of fs.readdirSync(iconDir)) {
for (const file of fs.readdirSync(path.resolve(iconDir, dir))) {
addIcon(dir, `material:${file.replace(/\..*?$/, "")}`, path.resolve(iconDir, dir, file));
}
}
// Add Custom Icons
iconDir = path.resolve(__dirname, "src/icons");
for (const icon of fs.readdirSync(iconDir)) {
addIcon(icon.replace(/\..*?$/, ""), "custom", path.resolve(iconDir, icon));
}
const icons = new Icons(_icons);
/** @type {import('webpack').Configuration} */
const config = {
mode: "development",
entry: [path.resolve(__dirname, "src/js/main.js"), path.resolve(__dirname, "src/styles/main.scss"), path.resolve(__dirname, "src/styles/Colors.scss")],
output: {
path: path.resolve(__dirname, "dist"),
filename: "dribbblish-dynamic.js"
},
module: {
rules: [
{
include: path.resolve(__dirname, "src/js/main.js"),
use: []
},
{
include: path.resolve(__dirname, "src/styles/main.scss"),
type: "asset/resource",
generator: {
filename: "user.css"
},
use: [
{
loader: "sass-loader",
options: {
implementation: sass,
sourceMap: true,
sassOptions: {
functions: {
"lightOffset($n, $offset)": (n, offset) => {
return new sass.types.String(lightOffset(n.getValue(), offset.getValue()));
},
"spiceColor($key, $alpha: 1, $light-offset: 0)": (key, alpha, _lightOffset) => {
return new sass.types.String(spiceColor(key.getValue(), alpha.getValue(), _lightOffset.getValue()));
},
"font64($font)": (font) => {
const file = path.resolve(__dirname, "src/fonts", font.getValue());
return new sass.types.String(`"data:font/truetype;charset=utf-8;base64,${fs.readFileSync(file, { encoding: "base64" })}"`);
},
'icon64($name, $options: "{}")': (name, options) => {
name = name.getValue();
options = JSON.parse(options.getValue());
return new sass.types.String(icons.get(name, { ...options, base64: true }));
}
}
}
}
}
]
},
{
include: path.resolve(__dirname, "src/styles/Colors.scss"),
type: "asset/resource",
generator: {
filename: "color.ini"
},
use: [path.resolve(__dirname, "src/loaders/color-loader.js")]
}
]
},
devtool: "inline-source-map",
plugins: [
new webpack.DefinePlugin({
"process.env.BUG_REPORT": JSON.stringify(
fs
.readFileSync(path.resolve(__dirname, ".github/ISSUE_TEMPLATE/bug_report.md"), { encoding: "utf8" })
.replace(/^---(.*?\r?\n)+---(.*?\r?\n)*?(?=\S)/, "") // Replace GitHub header
.replace(/\*\*Desktop Setup\*\*\r?\n(- .*?\r?\n)+\r?\n/, "") // Replace **Desktop Setup** block
),
"process.env.DRIBBBLISH_ICONS": JSON.stringify(_icons),
"process.env.DRIBBBLISH_VERSION": JSON.stringify(process.env.DRIBBBLISH_VERSION ?? "Dev"),
"process.env.COMMIT_HASH": JSON.stringify(process.env.COMMIT_HASH ?? "local")
}),
new CleanWebpackPlugin({
protectWebpackAssets: false,
cleanAfterEveryBuildPatterns: ["*.LICENSE.txt"]
})
]
};
export default config;

View file

@ -1,71 +0,0 @@
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"), path.resolve(__dirname, "./src/styles/Colors.scss")],
output: {
path: path.resolve(__dirname, "dist"),
filename: "dribbblish-dynamic.js"
},
module: {
rules: [
{
test: /main\.js$/,
exclude: /node_modules/,
use: []
},
{
test: /main\.scss$/,
exclude: /node_modules/,
type: "asset/resource",
generator: {
filename: "user.css"
},
use: [
{
loader: "sass-loader",
options: {
sourceMap: true
}
}
]
},
{
test: /Colors\.scss$/,
exclude: /node_modules/,
type: "asset/resource",
generator: {
filename: "color.ini"
},
use: [path.resolve(__dirname, "./src/loaders/color-loader.js")]
},
{
test: /\.svg/,
exclude: /node_modules/,
use: ["raw-loader"]
}
]
},
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"),
"process.env.COMMIT_HASH": JSON.stringify(process.env.COMMIT_HASH || "local")
}),
new CopyPlugin({
patterns: [{ from: "src/assets", to: "assets" }]
})
],
optimization: {
minimize: true,
minimizer: [`...`, new CssMinimizerPlugin()]
}
};