├── .all-contributorsrc
├── .eslintrc.json
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── media
├── icon.png
├── screenshot.png
└── screenshot2.png
├── package-lock.json
├── package.json
├── resources
├── dark
│ ├── logo.svg
│ └── refresh.svg
└── light
│ └── refresh.svg
├── src
├── actions
│ ├── actions.ts
│ └── common.ts
├── auth
│ └── server
│ │ └── local.ts
├── commands.ts
├── components
│ ├── spotify-controls.ts
│ ├── spotify-status.ts
│ ├── tree-albums.ts
│ ├── tree-playlists.ts
│ └── tree-track.ts
├── config
│ └── spotify-config.ts
├── consts
│ └── consts.ts
├── extension.ts
├── info
│ └── info.ts
├── isAlbum.ts
├── lyrics
│ └── lyrics.ts
├── reducers
│ └── root-reducer.ts
├── request
│ └── request.ts
├── spotify-status-controller.ts
├── spotify
│ ├── common.ts
│ ├── linux-spotify-client.ts
│ ├── os-agnostic-spotify-client.ts
│ ├── osx-spotify-client.ts
│ ├── spotify-client.ts
│ ├── utils.ts
│ └── web-api-spotify-client.ts
├── state
│ └── state.ts
├── store
│ ├── storage
│ │ └── vscode-storage.ts
│ └── store.ts
├── types
│ └── spotify-node-applescript.d.ts
└── utils
│ └── utils.ts
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "vscode-spotify",
3 | "projectOwner": "ShyykoSerhiy",
4 | "badgeTemplate": "[](#contributors)",
5 | "files": [
6 | "README.md"
7 | ],
8 | "imageSize": 100,
9 | "commit": true,
10 | "contributors": [
11 | {
12 | "login": "ShyykoSerhiy",
13 | "name": "shyyko.serhiy",
14 | "avatar_url": "https://avatars1.githubusercontent.com/u/1106995?v=4",
15 | "profile": "https://github.com/ShyykoSerhiy",
16 | "contributions": [
17 | "code",
18 | "design",
19 | "doc",
20 | "ideas",
21 | "review"
22 | ]
23 | },
24 | {
25 | "login": "levrik",
26 | "name": "Levin Rickert",
27 | "avatar_url": "https://avatars3.githubusercontent.com/u/9491603?v=4",
28 | "profile": "https://www.levrik.io",
29 | "contributions": [
30 | "bug",
31 | "code"
32 | ]
33 | },
34 | {
35 | "login": "mrcasals",
36 | "name": "Marc Riera",
37 | "avatar_url": "https://avatars3.githubusercontent.com/u/491891?v=4",
38 | "profile": "https://github.com/mrcasals",
39 | "contributions": [
40 | "code"
41 | ]
42 | },
43 | {
44 | "login": "ecbrodie",
45 | "name": "Evan Brodie",
46 | "avatar_url": "https://avatars2.githubusercontent.com/u/1844664?v=4",
47 | "profile": "https://github.com/ecbrodie",
48 | "contributions": [
49 | "code",
50 | "bug"
51 | ]
52 | },
53 | {
54 | "login": "stephanyan",
55 | "name": "Stéphane",
56 | "avatar_url": "https://avatars1.githubusercontent.com/u/5939522?v=4",
57 | "profile": "https://github.com/stephanyan",
58 | "contributions": [
59 | "doc"
60 | ]
61 | },
62 | {
63 | "login": "Ryan-Gordon",
64 | "name": "Ryan Gordon",
65 | "avatar_url": "https://avatars1.githubusercontent.com/u/11082710?v=4",
66 | "profile": "https://github.com/Ryan-Gordon",
67 | "contributions": [
68 | "doc",
69 | "ideas"
70 | ]
71 | },
72 | {
73 | "login": "audstanley",
74 | "name": "Richard Stanley",
75 | "avatar_url": "https://avatars0.githubusercontent.com/u/2934052?v=4",
76 | "profile": "http://www.audstanley.com",
77 | "contributions": [
78 | "code"
79 | ]
80 | },
81 | {
82 | "login": "realbizkit",
83 | "name": "realbizkit",
84 | "avatar_url": "https://avatars1.githubusercontent.com/u/25567148?v=4",
85 | "profile": "https://github.com/realbizkit",
86 | "contributions": [
87 | "code"
88 | ]
89 | },
90 | {
91 | "login": "xeBuz",
92 | "name": "Jesús Roldán",
93 | "avatar_url": "https://avatars1.githubusercontent.com/u/662916?v=4",
94 | "profile": "http://jesusroldan.com",
95 | "contributions": [
96 | "code"
97 | ]
98 | },
99 | {
100 | "login": "negebauer",
101 | "name": "Nicolás Gebauer",
102 | "avatar_url": "https://avatars3.githubusercontent.com/u/11860880?v=4",
103 | "profile": "https://negebauer.com",
104 | "contributions": [
105 | "code"
106 | ]
107 | },
108 | {
109 | "login": "mikqi",
110 | "name": "Muhammad Rivki",
111 | "avatar_url": "https://avatars0.githubusercontent.com/u/4416419?v=4",
112 | "profile": "http://this.rivki.id/",
113 | "contributions": [
114 | "code"
115 | ]
116 | },
117 | {
118 | "login": "miguelBinpar",
119 | "name": "Miguel Rodríguez Rosales",
120 | "avatar_url": "https://avatars0.githubusercontent.com/u/14270461?v=4",
121 | "profile": "https://github.com/miguelBinpar",
122 | "contributions": [
123 | "code"
124 | ]
125 | },
126 | {
127 | "login": "moshfeu",
128 | "name": "Mosh Feu",
129 | "avatar_url": "https://avatars2.githubusercontent.com/u/3723951?v=4",
130 | "profile": "https://il.linkedin.com/in/moshefeuchtwanger",
131 | "contributions": [
132 | "doc"
133 | ]
134 | },
135 | {
136 | "login": "pzelnip",
137 | "name": "Adam Parkin",
138 | "avatar_url": "https://avatars1.githubusercontent.com/u/414933?v=4",
139 | "profile": "https://www.codependentcodr.com",
140 | "contributions": [
141 | "doc"
142 | ]
143 | },
144 | {
145 | "login": "AndrewBastin",
146 | "name": "Andrew Bastin",
147 | "avatar_url": "https://avatars2.githubusercontent.com/u/9131943?v=4",
148 | "profile": "https://github.com/AndrewBastin",
149 | "contributions": [
150 | "doc"
151 | ]
152 | },
153 | {
154 | "login": "misterfoxy",
155 | "name": "Michael Fox",
156 | "avatar_url": "https://avatars2.githubusercontent.com/u/21694891?v=4",
157 | "profile": "https://www.michaelscottfox.com",
158 | "contributions": [
159 | "doc"
160 | ]
161 | },
162 | {
163 | "login": "matijamrkaic",
164 | "name": "Matija Mrkaic",
165 | "avatar_url": "https://avatars1.githubusercontent.com/u/2392130?v=4",
166 | "profile": "http://korra.io/",
167 | "contributions": [
168 | "code"
169 | ]
170 | },
171 | {
172 | "login": "marioortizmanero",
173 | "name": "Mario",
174 | "avatar_url": "https://avatars2.githubusercontent.com/u/25647296?v=4",
175 | "profile": "https://github.com/marioortizmanero",
176 | "contributions": [
177 | "doc"
178 | ]
179 | },
180 | {
181 | "login": "TheFern2",
182 | "name": "Fernando B",
183 | "avatar_url": "https://avatars3.githubusercontent.com/u/10265682?v=4",
184 | "profile": "http://www.kodaman.dev/",
185 | "contributions": [
186 | "code"
187 | ]
188 | }
189 | ],
190 | "repoType": "github",
191 | "commitConvention": "none"
192 | }
193 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": "warn",
13 | "@typescript-eslint/semi": "warn",
14 | "curly": "warn",
15 | "eqeqeq": "warn",
16 | "no-throw-literal": "warn",
17 | "semi": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_STORE
2 | .vscode-test
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (http://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # Typescript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # output directory for Typescript
64 | out
65 |
66 | # built vscode extension
67 | *.vsix
68 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | },
20 | {
21 | "name": "Extension Tests",
22 | "type": "extensionHost",
23 | "request": "launch",
24 | "args": [
25 | "--extensionDevelopmentPath=${workspaceFolder}",
26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
27 | ],
28 | "outFiles": [
29 | "${workspaceFolder}/out/test/**/*.js"
30 | ],
31 | "preLaunchTask": "${defaultBuildTask}"
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off",
11 | "typescript.tsdk": "node_modules/typescript/lib"
12 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | out/test/**
4 | src/**
5 | .gitignore
6 | .yarnrc
7 | vsc-extension-quickstart.md
8 | **/tsconfig.json
9 | **/.eslintrc.json
10 | **/*.map
11 | **/*.ts
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## [3.2.1] - 2020-11-07
8 | ### Fixed
9 | - Linux implementation of spotify client based on dbus is broken. #141 (kudos to TheFern2 (Fernando B) for debugging the issue).
10 |
11 | ## [3.2.0] - 2020-10-02
12 | ### Added
13 | - Seek to position in currently playing track #119
14 | - Include song title on lyrics sheet
15 | ### Fixed
16 | - Spotify in sidebar expanding on start #78
17 | - Lyrics functionality doesn't work #133
18 |
19 | ## [3.1.0] - 2018-11-20
20 | ### Added
21 | - Focus on song #56
22 | ### Fixed
23 | - Do not poll spotify if window is not focused on Mac OS (reducing CPU load).
24 | - Smaller icon size in Activity bar #58
25 |
26 | ## [3.0.4] - 2018-10-07
27 | ### Fixed
28 | - Invalid version in changelog.
29 |
30 | ## [3.0.3] - 2018-10-07
31 | ### Fixed
32 | - Invalid showLyricsButtonSignInButton, showLyricsButtonSignOutButton params.
33 |
34 | ## [3.0.2] - 2018-10-07
35 | ### Fixed
36 | - Missing option for show signIn and signOut button #54 (kudos to negebauer(Nicolás Gebauer))
37 |
38 | ## [3.0.1] - 2018-10-07
39 | ### Fixed
40 | - Tooltip Missing Album Name in Tree Track. #52 (kudos to mikqi(Muhammad Rivki))
41 |
42 | ## [3.0.0] - 2018-10-05
43 | ### Added
44 | - Spotify Web Api implementation to enable this extension on Windows.
45 | - Playlist/tracks custom view TreeDataProviders.
46 | ### Refactored
47 | - Refactored state to use Redux.
48 | ### Fixed
49 | - Spotilocal is not initialized. #46
50 | - Installed vscode-spotify extension but not working #43
51 | - Status Bar Icons Disappeared #42
52 | - Utilize the new Spotify Player API #27
53 |
54 | ## [2.6.1] - 2018-08-30
55 | ### Fixed
56 | - Fixed Play, Pause, PlayPause, Volume controlls on linux. ONLY on Linux(dbus)
57 | - Fixed performance issue on MacOs(to manny querys)
58 |
59 | ## [2.6.0] - 2018-08-27
60 | ### Fixed
61 | - Fix for Status Bar Icons Disappeared (#42). ONLY on Linux(dbus)
62 |
63 | ## [2.5.1] - 2018-07-23
64 | ### Fixed
65 | - Emergency fix for Status Bar Icons Disappeared (#42). ONLY on MacOS
66 |
67 | ## [2.5.0] - 2018-06-26
68 | ### Fixed
69 | - Use dbus on Linux (improved linux support) (#14) (kudos to audstanley(Richard Stanley)(https://github.com/audstanley))
70 |
71 | ## [2.4.0] - 2018-03-25
72 | ### Fixed
73 | - Change default mac keybindings for play previous/next (#34) (kudos to ecbrodie(Evan Brodie)(https://github.com/ecbrodie)) @see the reasoning behind this at https://github.com/ShyykoSerhiy/vscode-spotify/pull/34
74 |
75 | ## [2.3.2] - 2018-02-23
76 | ### Fixed
77 | - `trackInfoPriority` setting has no effect (#30) (kudos to realbizkit(András Szepes)(https://github.com/realbizkit))
78 |
79 | ## [2.3.1] - 2018-02-09
80 | ### Fixed
81 | - Increasing volume at max makes it go to 0 (#15)
82 |
83 | ## [2.3.0] - 2018-02-06
84 | ### Added
85 | - Show album title (via config spotify.trackInfoFormat) #28 (kudos to @mrcasals(Marc Riera) (https://github.com/mrcasals))
86 | - Lyrics in split panel window (via spotify.openPanelLyrics) #22 (kudos to @xeBuz(Jesús Roldán) (https://github.com/xeBuz))
87 |
88 | ## [2.2.1] - 2017-10-10
89 | ### Fixed
90 | - Remember last successfully used port (initialize speed up) #21 (kudos to @levrik(Levin Rickert) (https://github.com/levrik))
91 |
92 | ## [2.2.0] - 2017-09-30
93 | ### Fixed
94 | - When Spotify is not open: "Failed to initialize vscode-spotify. We'll keep trying every 20 seconds." #20
95 | ### Changed
96 | - useCombinedApproachOnMacOS is now true by default.
97 |
98 | ## [2.1.1] - 2017-09-24
99 | ### Fixed
100 | - Updated info in Readme.md
101 |
102 | ## [2.1.0] - 2017-09-24
103 | ### Added
104 | - Implemented status long-polling on Windows and Linux #19 (kudos to @levrik(Levin Rickert) (https://github.com/levrik))
105 | - Experimental applescript + http implementation of spotify client to reduce CPU usage on MacOs and improve status updating. Set spotify.useCombinedApproachOnMacOS to true to try out! (fixes https://github.com/ShyykoSerhiy/vscode-spotify/issues/12)
106 | ### Fixed
107 | - High CPU usage on MacOS #12 (via spotify.useCombinedApproachOnMacOS)
108 |
109 | ## [2.0.1] - 2017-09-23
110 | ### Fixed
111 | - Failed to initialize vscode-spotify. We'll keep trying every 20 seconds. (New VSCode Insiders / New Spotify ?) #17
112 |
113 | ## [2.0.0] - 2017-05-22
114 | ### Added
115 | - Added lyrics button(via Genius API). @see https://github.com/shyykoserhiy/vscode-spotify-lyrics
116 | - Added prefix 'vscode-spotify' for all messages from this extension.
117 |
118 | ## [1.1.0] - 2017-02-27
119 | ### Added
120 | - Added statusCheckInterval param.
121 |
122 | ## [1.0.0] - 2016-09-14
123 | ### Added
124 | - initial implementation of windows & linux support.
125 |
126 | ## [0.0.5] - 2015-12-08
127 | ### Added
128 | - added dynamic colors for buttons
129 | - added spotify.toggleRepeating and spotify.toggleShuffling commands
130 | - added buttons to status bar
131 |
132 | ### Fixed
133 | - Extension reopens Spotify if it gets closed. #4
134 | - Sometimes there is error (unexpected tocken u), that hides all the buttons. #3
135 | - Wrong stopped/playing state #2
136 |
137 | ## [0.0.2] - 2015-11-25
138 | ### Added
139 | - initial release
140 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 shyyko.serhiy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://marketplace.visualstudio.com/items?itemName=shyykoserhiy.vscode-spotify)
2 | [](https://marketplace.visualstudio.com/items?itemName=shyykoserhiy.vscode-spotify)
3 | [](https://marketplace.visualstudio.com/items?itemName=shyykoserhiy.vscode-spotify#review-details)
4 |
5 | # vscode-spotify
6 |
7 | [](#contributors)
8 |
9 | Use Spotify inside vscode.
10 | Provides integration with Spotify Desktop client.
11 |
12 | Note that some of the functionalities are only available on macOS systems (see [How it works section](#how-it-works))
13 |
14 | **This extension requires Spotify Premium to work on Windows**
15 |
16 | ## How it works
17 |
18 | * On macOS, this extension uses [spotify-node-applescript](https://github.com/andrehaveman/spotify-node-applescript) (basically a wrapper for the official Spotify AppleScript API) to communicate with Spotify.
19 | * On Windows, the extension uses the Spotify Web API.
20 | * On Linux, it uses a combination of dbus and pactl.
21 |
22 | Spotify Web API implementation can be used on any platform, but it does have some drawbacks:
23 | * It doesn't work without internet connection (Linux and OS X implementations do).
24 | * Full functionality is only available to Spotify Premium users.
25 | * API calls are rate limited.
26 |
27 | At the same time it provides tighter integration and it's more or less future proofed.
28 |
29 | ## Features
30 | * Shows the currently playing song in the vscode status bar.
31 | 
32 | * Provides [commands](#commands) for controlling Spotify from vscode.
33 | * Provides [hotkeys](#Adding%20or%20changing%20hotkeys) of commands.
34 | * Provides [buttons](#buttons) for controlling Spotify from vscode.
35 |
36 | ## Compatibility table
37 |
38 | | Feature | macOS | Linux | Any Platform (only option on Windows) Web API |
39 | | ---------------------------- |:-------------:| :--------------------------- | :-------------------------------------------- |
40 | | Works Offline | ✅ | ✅ | ❌ |
41 | | Show Current Song | ✅ | ✅ | ✅ |
42 | | Play Next Song | ✅ | ✅ | ✅ |
43 | | Play Previous Song | ✅ | ✅ | ✅ |
44 | | Play | ✅ | ✅ | ✅ |
45 | | Pause | ✅ | ✅ | ✅ |
46 | | Play Pause | ✅ | ✅ | ✅ |
47 | | Mute Volume | ✅ | ✅ | ✅ |
48 | | Unmute Volume | ✅ | ✅ | ✅ |
49 | | Mute Unmute Volume | ✅ | ✅ | ✅ |
50 | | Volume Up | ✅ | ✅ | ✅ |
51 | | Volume Down | ✅ | ✅ | ✅ |
52 | | Toggle Repeating | ✅ | (shows repeating state) ⭕ | ✅ |
53 | | Toggle Shuffling | ✅ | (shows shuffling state) ⭕ | ✅ |
54 | | Lyrics | ✅ | ✅ | ✅ |
55 |
56 | Additional Web API features:
57 | * Playlists/tracks selection. *Make sure you have logged in with the command `>Spotify Sign In` to use these features. You can open the Virtual Studio Code command line with the hotkey `Ctrl+P` by default.*
58 |
59 | ## Contributing
60 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome, any contributions made will be recognised in the README.
61 |
62 | A list of contributors to this project ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
63 |
64 |
65 |
66 |
67 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | ## Commands
101 |
102 | There are a number of commands available for the extension, and accessible via the command
103 | pallette. Find them by searching for "Spotify" in the command pallette:
104 |
105 | 
106 |
107 | ## Adding or changing hotkeys
108 | All keyboard shortcuts in vscode can be customized via the `User/keybindings.json` file.
109 |
110 | To configure keyboard shortcuts the way you want, go to the menu under File > Preferences > Keyboard Shortcuts.
111 | This will open the Default Keyboard Shortcuts on the left and your `User/keybindings.json` file where you can overwrite the default bindings on the right. You may also see an interface to modify the shortcuts on different versions of vscode.
112 |
113 | Example :
114 | ```json
115 | {
116 | "command": "spotify.volumeDown",
117 | "key": "cmd+shift+g"
118 | }
119 | ```
120 |
121 | For more info on hotkeys please look at https://code.visualstudio.com/docs/customization/keybindings
122 |
123 | ## Buttons
124 | This extension provides a variety of buttons to control Spotify from status bar. By default 4 buttons are enabled:
125 | 1. Previous track
126 | 2. Play / Pause
127 | 3. Next track
128 | 4. Mute / Unmute
129 |
130 | You can modify the shown buttons by changing your parameters ([go here to find out how](https://code.visualstudio.com/docs/customization/userandworkspace)):
131 | ```json
132 | "spotify.showNextButton": {
133 | "type": "boolean",
134 | "default": true,
135 | "description": "Whether to show next button."
136 | },
137 | "spotify.showPreviousButton": {
138 | "type": "boolean",
139 | "default": true,
140 | "description": "Whether to show previous button."
141 | },
142 | "spotify.showPlayButton": {
143 | "type": "boolean",
144 | "default": false,
145 | "description": "Whether to show play button."
146 | },
147 | "spotify.showPauseButton": {
148 | "type": "boolean",
149 | "default": false,
150 | "description": "Whether to show pause button."
151 | },
152 | "spotify.showPlayPauseButton": {
153 | "type": "boolean",
154 | "default": true,
155 | "description": "Whether to show play|pause button."
156 | },
157 | "spotify.showMuteVolumeButton": {
158 | "type": "boolean",
159 | "default": false,
160 | "description": "Whether to show mute button."
161 | },
162 | "spotify.showUnmuteVolumeButton": {
163 | "type": "boolean",
164 | "default": false,
165 | "description": "Whether to show unmute button."
166 | },
167 | "spotify.showMuteUnmuteVolumeButton": {
168 | "type": "boolean",
169 | "default": true,
170 | "description": "Whether to show mute|unmute button."
171 | },
172 | "spotify.showVolumeUpButton": {
173 | "type": "boolean",
174 | "default": false,
175 | "description": "Whether to show volume up button."
176 | },
177 | "spotify.showVolumeDownButton": {
178 | "type": "boolean",
179 | "default": false,
180 | "description": "Whether to show volume down button."
181 | }
182 | "spotify.showToggleRepeatingButton": {
183 | "type": "boolean",
184 | "default": false,
185 | "description": "Whether to show toggle repeating button."
186 | },
187 | "spotify.showToggleShufflingButton": {
188 | "type": "boolean",
189 | "default": false,
190 | "description": "Whether to show toggle shuffling button."
191 | }
192 | ```
193 |
194 | For the full configuration options go [here](https://github.com/ShyykoSerhiy/vscode-spotify/blob/master/package.json#L161).
195 |
196 | Note that due to limitations of Spotify's Applescript API ```toggleRepeatingButton``` toggles only
197 | 'repeat all' property of spotify. There is no way to set 'repeat one' via vscode-spotify.
198 |
199 | You can also change the position of buttons by changing the parameters below:
200 |
201 | ```json
202 | "spotify.priorityBase": {
203 | "type": "number",
204 | "default": 30,
205 | "description": "Base value of priority for all vscode-spotify elements in Status Bar(priority = basePriority+priority). This is done to avoid 'conflicts' with other extensions. "
206 | },
207 | "spotify.nextButtonPriority": {
208 | "type": "number",
209 | "default": 8,
210 | "description": "Priority of next button."
211 | },
212 | "spotify.previousButtonPriority": {
213 | "type": "number",
214 | "default": 10,
215 | "description": "Priority of previous button."
216 | },
217 | "spotify.playButtonPriority": {
218 | "type": "number",
219 | "default": 7,
220 | "description": "Priority of play button."
221 | },
222 | "spotify.pauseButtonPriority": {
223 | "type": "number",
224 | "default": 6,
225 | "description": "Priority of pause button."
226 | },
227 | "spotify.playPauseButtonPriority": {
228 | "type": "number",
229 | "default": 9,
230 | "description": "Priority of play|pause button."
231 | },
232 | "spotify.muteButtonPriority": {
233 | "type": "number",
234 | "default": 5,
235 | "description": "Priority of mute button."
236 | },
237 | "spotify.unmuteButtonPriority": {
238 | "type": "number",
239 | "default": 4,
240 | "description": "Priority of unmute button."
241 | },
242 | "spotify.muteUnmuteButtonPriority": {
243 | "type": "number",
244 | "default": 3,
245 | "description": "Priority of mute|unmute button."
246 | },
247 | "spotify.volumeUpButtonPriority": {
248 | "type": "number",
249 | "default": 2,
250 | "description": "Priority of volume up button."
251 | },
252 | "spotify.volumeDownButtonPriority": {
253 | "type": "number",
254 | "default": 1,
255 | "description": "Priority of volume down button."
256 | },
257 | "spotify.trackInfoPriority": {
258 | "type": "number",
259 | "default": 0,
260 | "description": "Priority of volume track info."
261 | },
262 | "spotify.toggleRepeatingButtonPriority": {
263 | "type": "number",
264 | "default": 11,
265 | "description": "Priority of toggle repeating button."
266 | },
267 | "spotify.toggleShufflingButtonPriority": {
268 | "type": "number",
269 | "default": 12,
270 | "description": "Priority of toggle shuffling button."
271 | }
272 | ```
273 |
274 | ## Seeking to a specific point in a song
275 | You can use `Spotify Seek To` command (`spotify.seekTo`) to seek to the specific point in a song. You can specify hotkey that will seek to a custom time in a song by adding keybinding in keybindings.json. For example:
276 |
277 | ```json
278 | {
279 | "command": "spotify.seekTo",
280 | "key": "alt+d",
281 | "args": "1:15"
282 | }
283 | ```
284 |
285 | [MIT LICENSE](LICENSE)
286 |
--------------------------------------------------------------------------------
/media/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShyykoSerhiy/vscode-spotify/bdc1750523294c2331da47074a340558c0daae27/media/icon.png
--------------------------------------------------------------------------------
/media/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShyykoSerhiy/vscode-spotify/bdc1750523294c2331da47074a340558c0daae27/media/screenshot.png
--------------------------------------------------------------------------------
/media/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShyykoSerhiy/vscode-spotify/bdc1750523294c2331da47074a340558c0daae27/media/screenshot2.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vscode-spotify",
3 | "description": "Use Spotify inside vscode.",
4 | "version": "3.2.1",
5 | "publisher": "shyykoserhiy",
6 | "license": "MIT",
7 | "engines": {
8 | "vscode": "^1.49.0"
9 | },
10 | "icon": "media/icon.png",
11 | "galleryBanner": {
12 | "color": "#11B460",
13 | "theme": "light"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/ShyykoSerhiy/vscode-spotify.git"
18 | },
19 | "categories": [
20 | "Other"
21 | ],
22 | "activationEvents": [
23 | "*"
24 | ],
25 | "contributes": {
26 | "viewsContainers": {
27 | "activitybar": [
28 | {
29 | "id": "vscode-spotify",
30 | "title": "vscode-spotify",
31 | "icon": "resources/dark/logo.svg"
32 | }
33 | ]
34 | },
35 | "views": {
36 | "vscode-spotify": [
37 | {
38 | "id": "vscode-spotify-playlists",
39 | "name": "Playlists"
40 | },
41 | {
42 | "id": "vscode-spotify-albums",
43 | "name": "Albums"
44 | },
45 | {
46 | "id": "vscode-spotify-tracks",
47 | "name": "Tracks"
48 | }
49 | ]
50 | },
51 | "commands": [
52 | {
53 | "command": "spotify.next",
54 | "title": "Spotify Play Next Song"
55 | },
56 | {
57 | "command": "spotify.previous",
58 | "title": "Spotify Play Previous Song"
59 | },
60 | {
61 | "command": "spotify.play",
62 | "title": "Spotify Play"
63 | },
64 | {
65 | "command": "spotify.pause",
66 | "title": "Spotify Pause"
67 | },
68 | {
69 | "command": "spotify.playPause",
70 | "title": "Spotify Play|Pause"
71 | },
72 | {
73 | "command": "spotify.muteVolume",
74 | "title": "Spotify Mute Volume"
75 | },
76 | {
77 | "command": "spotify.unmuteVolume",
78 | "title": "Spotify Unmute Volume"
79 | },
80 | {
81 | "command": "spotify.muteUnmuteVolume",
82 | "title": "Spotify Mute|Unmute Volume"
83 | },
84 | {
85 | "command": "spotify.volumeUp",
86 | "title": "Spotify Volume Up"
87 | },
88 | {
89 | "command": "spotify.volumeDown",
90 | "title": "Spotify Volume Down"
91 | },
92 | {
93 | "command": "spotify.toggleRepeating",
94 | "title": "Spotify Toggle Repeating"
95 | },
96 | {
97 | "command": "spotify.toggleShuffling",
98 | "title": "Spotify Toggle Shuffling"
99 | },
100 | {
101 | "command": "spotify.lyrics",
102 | "title": "Spotify Lyrics"
103 | },
104 | {
105 | "command": "spotify.signIn",
106 | "title": "Spotify Sign In"
107 | },
108 | {
109 | "command": "spotify.signOut",
110 | "title": "Spotify Sign Out"
111 | },
112 | {
113 | "command": "spotify.loadPlaylists",
114 | "title": "Spotify Load Playlists",
115 | "icon": {
116 | "light": "resources/light/refresh.svg",
117 | "dark": "resources/dark/refresh.svg"
118 | }
119 | },
120 | {
121 | "command": "spotify.loadAlbums",
122 | "title": "Spotify Load Albums",
123 | "icon": {
124 | "light": "resources/light/refresh.svg",
125 | "dark": "resources/dark/refresh.svg"
126 | }
127 | },
128 | {
129 | "command": "spotify.loadTracks",
130 | "title": "Spotify Load Tracks",
131 | "icon": {
132 | "light": "resources/light/refresh.svg",
133 | "dark": "resources/dark/refresh.svg"
134 | }
135 | },
136 | {
137 | "command": "spotify.trackInfoClick",
138 | "title": "Spotify TrackInfo Click"
139 | },
140 | {
141 | "command": "spotify.seekTo",
142 | "title": "Spotify Seek To"
143 | }
144 | ],
145 | "keybindings": [
146 | {
147 | "command": "spotify.next",
148 | "key": "ctrl+shift+]",
149 | "mac": "ctrl+cmd+]"
150 | },
151 | {
152 | "command": "spotify.previous",
153 | "key": "ctrl+shift+[",
154 | "mac": "ctrl+cmd+["
155 | },
156 | {
157 | "command": "spotify.volumeUp",
158 | "key": "ctrl+shift+'",
159 | "mac": "cmd+shift+'"
160 | },
161 | {
162 | "command": "spotify.volumeDown",
163 | "key": "ctrl+shift+;",
164 | "mac": "cmd+shift+;"
165 | }
166 | ],
167 | "menus": {
168 | "view/title": [
169 | {
170 | "command": "spotify.loadPlaylists",
171 | "when": "view == vscode-spotify-playlists",
172 | "group": "navigation"
173 | },
174 | {
175 | "command": "spotify.loadAlbums",
176 | "when": "view == vscode-spotify-albums",
177 | "group": "navigation"
178 | },
179 | {
180 | "command": "spotify.loadTracks",
181 | "when": "view == vscode-spotify-tracks",
182 | "group": "navigation"
183 | }
184 | ]
185 | },
186 | "configuration": {
187 | "type": "object",
188 | "title": "vscode-spotify configuration",
189 | "properties": {
190 | "spotify.trackInfoFormat": {
191 | "type": "string",
192 | "default": "artistName - trackName",
193 | "description": "Current track info that will be displayed. Available keywords: albumName, artistName, trackName"
194 | },
195 | "spotify.trackInfoClickBehaviour": {
196 | "type": "string",
197 | "enum": [
198 | "none",
199 | "focus_song",
200 | "play_pause"
201 | ],
202 | "default": "focus_song",
203 | "description": "What to do when trackInfo is clicked: 'none' - do nothing, 'focus_song' - current song will be selected Playlists/Tracks section if it exists there, 'play_pause' - trackInfo essentially becomes playPause button (great option for minimal ui). "
204 | },
205 | "spotify.showNextButton": {
206 | "type": "boolean",
207 | "default": true,
208 | "description": "Whether to show next button."
209 | },
210 | "spotify.showPreviousButton": {
211 | "type": "boolean",
212 | "default": true,
213 | "description": "Whether to show previous button."
214 | },
215 | "spotify.showPlayButton": {
216 | "type": "boolean",
217 | "default": false,
218 | "description": "Whether to show play button."
219 | },
220 | "spotify.showPauseButton": {
221 | "type": "boolean",
222 | "default": false,
223 | "description": "Whether to show pause button."
224 | },
225 | "spotify.showPlayPauseButton": {
226 | "type": "boolean",
227 | "default": true,
228 | "description": "Whether to show play|pause button."
229 | },
230 | "spotify.showMuteVolumeButton": {
231 | "type": "boolean",
232 | "default": false,
233 | "description": "Whether to show mute button."
234 | },
235 | "spotify.showUnmuteVolumeButton": {
236 | "type": "boolean",
237 | "default": false,
238 | "description": "Whether to show unmute button."
239 | },
240 | "spotify.showMuteUnmuteVolumeButton": {
241 | "type": "boolean",
242 | "default": true,
243 | "description": "Whether to show mute|unmute button."
244 | },
245 | "spotify.showVolumeUpButton": {
246 | "type": "boolean",
247 | "default": false,
248 | "description": "Whether to show volume up button."
249 | },
250 | "spotify.showVolumeDownButton": {
251 | "type": "boolean",
252 | "default": false,
253 | "description": "Whether to show volume down button."
254 | },
255 | "spotify.showToggleRepeatingButton": {
256 | "type": "boolean",
257 | "default": false,
258 | "description": "Whether to show toggle repeating button."
259 | },
260 | "spotify.showToggleShufflingButton": {
261 | "type": "boolean",
262 | "default": false,
263 | "description": "Whether to show toggle shuffling button."
264 | },
265 | "spotify.showLyricsButton": {
266 | "type": "boolean",
267 | "default": true,
268 | "description": "Whether to show lyrics button."
269 | },
270 | "spotify.showSignInButton": {
271 | "type": "boolean",
272 | "default": true,
273 | "description": "Whether to show sign in button."
274 | },
275 | "spotify.showSignOutButton": {
276 | "type": "boolean",
277 | "default": true,
278 | "description": "Whether to show sign out button."
279 | },
280 | "spotify.openPanelLyrics": {
281 | "type": "number",
282 | "default": 1,
283 | "enum": [
284 | 1,
285 | 2,
286 | 3
287 | ],
288 | "description": "Panel to display the Lyrics"
289 | },
290 | "spotify.priorityBase": {
291 | "type": "number",
292 | "default": 30,
293 | "description": "Base value of priority for all vscode-spotify elements in Status Bar(priority = basePriority+priority). This is done to avoid 'conflicts' with other extensions. "
294 | },
295 | "spotify.nextButtonPriority": {
296 | "type": "number",
297 | "default": 8,
298 | "description": "Priority of next button."
299 | },
300 | "spotify.previousButtonPriority": {
301 | "type": "number",
302 | "default": 10,
303 | "description": "Priority of previous button."
304 | },
305 | "spotify.playButtonPriority": {
306 | "type": "number",
307 | "default": 7,
308 | "description": "Priority of play button."
309 | },
310 | "spotify.pauseButtonPriority": {
311 | "type": "number",
312 | "default": 6,
313 | "description": "Priority of pause button."
314 | },
315 | "spotify.playPauseButtonPriority": {
316 | "type": "number",
317 | "default": 9,
318 | "description": "Priority of play|pause button."
319 | },
320 | "spotify.muteButtonPriority": {
321 | "type": "number",
322 | "default": 5,
323 | "description": "Priority of mute button."
324 | },
325 | "spotify.unmuteButtonPriority": {
326 | "type": "number",
327 | "default": 4,
328 | "description": "Priority of unmute button."
329 | },
330 | "spotify.muteUnmuteButtonPriority": {
331 | "type": "number",
332 | "default": 3,
333 | "description": "Priority of mute|unmute button."
334 | },
335 | "spotify.volumeUpButtonPriority": {
336 | "type": "number",
337 | "default": 2,
338 | "description": "Priority of volume up button."
339 | },
340 | "spotify.volumeDownButtonPriority": {
341 | "type": "number",
342 | "default": 1,
343 | "description": "Priority of volume down button."
344 | },
345 | "spotify.trackInfoPriority": {
346 | "type": "number",
347 | "default": 0,
348 | "description": "Priority of track info."
349 | },
350 | "spotify.toggleRepeatingButtonPriority": {
351 | "type": "number",
352 | "default": 11,
353 | "description": "Priority of toggle repeating button."
354 | },
355 | "spotify.toggleShufflingButtonPriority": {
356 | "type": "number",
357 | "default": 12,
358 | "description": "Priority of toggle shuffling button."
359 | },
360 | "spotify.lyricsButtonPriority": {
361 | "type": "number",
362 | "default": 0,
363 | "description": "Priority of lyrics button."
364 | },
365 | "spotify.signInButtonPriority": {
366 | "type": "number",
367 | "default": 15,
368 | "description": "Priority of sign in button."
369 | },
370 | "spotify.signOutButtonPriority": {
371 | "type": "number",
372 | "default": 15,
373 | "description": "Priority of sign out button."
374 | },
375 | "spotify.statusCheckInterval": {
376 | "type": "number",
377 | "default": 5000,
378 | "description": "Interval of spotify status checks. Frequent status checks may result in faster battery drain and high CPU load (especially on MacOs). Note that with web api implementation 5000 is mimimal possible value."
379 | },
380 | "spotify.lyricsServerUrl": {
381 | "type": "string",
382 | "default": "https://vscode-spotify-lyrics.azurewebsites.net/v3/songs",
383 | "description": "Url for lyrics server. @see https://github.com/ShyykoSerhiy/vscode-spotify-lyrics ."
384 | },
385 | "spotify.authServerUrl": {
386 | "type": "string",
387 | "default": "https://vscode-spotify-auth.azurewebsites.net",
388 | "description": "Url to auth server. @see https://github.com/ShyykoSerhiy/vscode-spotify-auth"
389 | },
390 | "spotify.spotifyApiUrl": {
391 | "type": "string",
392 | "default": "https://api.spotify.com/v1",
393 | "description": "Url to spotify api server. @see https://beta.developer.spotify.com/documentation/web-api/reference/playlists/"
394 | },
395 | "spotify.forceWebApiImplementation": {
396 | "type": "boolean",
397 | "default": false,
398 | "description": "Whether to force web api implementation on Linux or MacOs. If you you want this enabled, please, consider using your own spotify.authServerUrl ."
399 | },
400 | "spotify.enableLogs": {
401 | "type": "boolean",
402 | "default": false,
403 | "description": "Whether to enable logs."
404 | }
405 | }
406 | }
407 | },
408 | "main": "./out/extension.js",
409 | "scripts": {
410 | "vscode:prepublish": "npm run compile",
411 | "compile": "tsc -p ./",
412 | "lint": "eslint src --ext ts",
413 | "watch": "tsc -watch -p ./",
414 | "pretest": "npm run compile && npm run lint",
415 | "madge:circular": "npx madge --circular --extensions ts ./src",
416 | "contributors:add": "all-contributors add",
417 | "contributors:generate": "all-contributors generate",
418 | "contributors:check": "all-contributors check"
419 | },
420 | "devDependencies": {
421 | "@types/cheerio": "^0.22.22",
422 | "@types/express": "^4.11.1",
423 | "@types/mocha": "^8.0.0",
424 | "@types/node": "^14.0.27",
425 | "@types/superagent": "^2.0.36",
426 | "@types/vscode": "^1.49.0",
427 | "@typescript-eslint/eslint-plugin": "^4.1.1",
428 | "@typescript-eslint/parser": "^4.1.1",
429 | "all-contributors-cli": "^6.17.4",
430 | "eslint": "^7.9.0",
431 | "typescript": "^4.0.2",
432 | "vscode-test": "^1.4.0"
433 | },
434 | "dependencies": {
435 | "@vscodespotify/spotify-common": "1.4.1",
436 | "autobind-decorator": "^2.1.0",
437 | "cheerio": "^1.0.0-rc.3",
438 | "child_process": "^1.0.2",
439 | "express": "^4.16.2",
440 | "immutable": "^3.8.2",
441 | "moment": "^2.22.2",
442 | "redux": "^3.7.2",
443 | "redux-persist": "^5.10.0",
444 | "request-light": "^0.2.1",
445 | "spotify-node-applescript": "1.1.1"
446 | }
447 | }
448 |
--------------------------------------------------------------------------------
/resources/dark/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/resources/dark/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/light/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/actions/actions.ts:
--------------------------------------------------------------------------------
1 | import { Api, getApi } from '@vscodespotify/spotify-common/lib/spotify/api';
2 | import { Album, Playlist, Track } from '@vscodespotify/spotify-common/lib/spotify/consts';
3 | import autobind from 'autobind-decorator';
4 | import { commands, Uri, window } from 'vscode';
5 |
6 | import { createDisposableAuthSever } from '../auth/server/local';
7 | import { getAuthServerUrl } from '../config/spotify-config';
8 | import { SIGN_IN_COMMAND } from '../consts/consts';
9 | import { log, showInformationMessage, showWarningMessage, showErrorMessage } from '../info/info';
10 | import { isAlbum } from '../isAlbum';
11 | import { DUMMY_PLAYLIST, ILoginState, ISpotifyStatusState } from '../state/state';
12 | import { getState, getStore } from '../store/store';
13 | import { artistsToArtist } from '../utils/utils';
14 | import {
15 | UpdateStateAction,
16 | UPDATE_STATE_ACTION,
17 | PlaylistsLoadAction,
18 | PLAYLISTS_LOAD_ACTION,
19 | AlbumLoadAction,
20 | ALBUM_LOAD_ACTION,
21 | SelectPlaylistAction,
22 | SELECT_PLAYLIST_ACTION,
23 | SelectAlbumAction,
24 | SELECT_ALBUM_ACTION,
25 | SelectTrackAction,
26 | SELECT_TRACK_ACTION,
27 | TracksLoadAction,
28 | TRACKS_LOAD_ACTION,
29 | SignInAction,
30 | SIGN_IN_ACTION,
31 | SignOutAction,
32 | SIGN_OUT_ACTION
33 | } from './common';
34 |
35 | export function withApi() {
36 | return (_target: any, _key: any, descriptor: PropertyDescriptor) => {
37 | const originalMethod = descriptor.value;
38 |
39 | descriptor.value = function(...args: any[]) {
40 | const api = getSpotifyWebApi();
41 | if (api) {
42 | return originalMethod.apply(this, [...args, api]);
43 | } else {
44 | (async () => {
45 | const signIn = 'Sign in';
46 | const result = await showWarningMessage('You should be logged in order to use this feature.', signIn);
47 | if (result === signIn) {
48 | commands.executeCommand(SIGN_IN_COMMAND);
49 | }
50 | })();
51 | }
52 | };
53 |
54 | return descriptor;
55 | };
56 | }
57 |
58 | export function withErrorAsync() {
59 | return (_target: any, _key: any, descriptor: PropertyDescriptor) => {
60 | const originalMethod = descriptor.value;
61 |
62 | descriptor.value = async function (...args: any[]) {
63 | try {
64 | return await originalMethod.apply(this, args);
65 | } catch (e) {
66 | showWarningMessage('Failed to perform operation ' + e.message || e);
67 | }
68 | };
69 |
70 | return descriptor;
71 | };
72 | }
73 |
74 | function actionCreator() {
75 | return (_target: any, _key: any, descriptor: PropertyDescriptor) => {
76 | const originalMethod = descriptor.value;
77 |
78 | descriptor.value = function (...args: any[]) {
79 | const action = originalMethod.apply(this, args);
80 | if (!action) {
81 | return;
82 | }
83 | getStore().dispatch(action);
84 | };
85 |
86 | return descriptor;
87 | };
88 | }
89 |
90 | function asyncActionCreator() {
91 | return (_target: any, _key: any, descriptor: PropertyDescriptor) => {
92 | const originalMethod = descriptor.value;
93 |
94 | descriptor.value = async function(...args: any[]) {
95 | let action;
96 | try {
97 | action = await originalMethod.apply(this, args);
98 | if (!action) {
99 | return;
100 | }
101 | } catch (e) {
102 | showWarningMessage('Failed to perform operation ' + e.message || e);
103 | }
104 | getStore().dispatch(action);
105 | };
106 |
107 | return descriptor;
108 | };
109 | }
110 |
111 | const apiMap = new WeakMap();
112 | export const getSpotifyWebApi = () => {
113 | const { loginState } = getState();
114 | if (!loginState) {
115 | log('getSpotifyWebApi', 'NOT LOGGED IN');
116 | return null;
117 | }
118 | if (!window.state.focused) {
119 | log('getSpotifyWebApi', 'NOT FOCUSED');
120 | return null;
121 | }
122 | let api = apiMap.get(loginState);
123 | if (!api) {
124 | api = getApi(getAuthServerUrl(), loginState.accessToken, loginState.refreshToken, (token: string) => {
125 | actionsCreator._actionSignIn(token, loginState.refreshToken);
126 | });
127 | apiMap.set(loginState, api);
128 | }
129 | return api;
130 | };
131 |
132 | class ActionCreator {
133 | @autobind
134 | @actionCreator()
135 | updateStateAction(state: Partial): UpdateStateAction {
136 | return {
137 | type: UPDATE_STATE_ACTION,
138 | state
139 | };
140 | }
141 |
142 | @autobind
143 | @asyncActionCreator()
144 | @withApi()
145 | async loadPlaylists(api?: Api): Promise {
146 | const playlists = await api!.playlists.getAll();
147 | return {
148 | type: PLAYLISTS_LOAD_ACTION,
149 | playlists
150 | };
151 | }
152 |
153 | @autobind
154 | @asyncActionCreator()
155 | @withApi()
156 | async loadAlbums(api?: Api): Promise {
157 | const albums = await api!.albums.getAll();
158 | return {
159 | type: ALBUM_LOAD_ACTION,
160 | albums
161 | };
162 | }
163 |
164 |
165 |
166 | @autobind
167 | @actionCreator()
168 | selectPlaylistAction(p: Playlist): SelectPlaylistAction {
169 | return {
170 | type: SELECT_PLAYLIST_ACTION,
171 | playlist: p
172 | };
173 | }
174 |
175 | @autobind
176 | @actionCreator()
177 | selectAlbumAction(album: Album): SelectAlbumAction {
178 | return {
179 | type: SELECT_ALBUM_ACTION,
180 | album
181 | };
182 | }
183 |
184 | @autobind
185 | @actionCreator()
186 | selectTrackAction(track: Track): SelectTrackAction {
187 | return {
188 | type: SELECT_TRACK_ACTION,
189 | track
190 | };
191 | }
192 |
193 | @autobind
194 | selectCurrentTrack() {
195 | const state = getState();
196 | if (state.playerState && state.track) {
197 | let track: Track;
198 | const currentTrack = state.track;
199 | const playlist = state.playlists.find(p => {
200 | const tracks = state.tracks.get(p.id);
201 | if (tracks) {
202 | const foundTrack = tracks.find(t => t.track.name === currentTrack.name
203 | && t.track.album.name === currentTrack.album
204 | && artistsToArtist(t.track.artists) === currentTrack.artist);
205 |
206 | if (foundTrack) {
207 | track = foundTrack;
208 | return true;
209 | }
210 | }
211 | return false;
212 | });
213 |
214 | if (playlist) {
215 | this.selectPlaylistAction(playlist);
216 | this.selectTrackAction(track!);
217 | }
218 | }
219 | }
220 |
221 | @autobind
222 | loadTracksForSelectedPlaylist(): void {
223 | this.loadTracks(getState().selectedList);
224 | }
225 |
226 | @autobind
227 | loadTracksIfNotLoaded(list: Playlist | Album): void {
228 | if (!list) {
229 | return void 0;
230 | }
231 | const { tracks } = getState();
232 | if (!tracks.has(isAlbum(list) ? list.album.id : list.id)) {
233 | this.loadTracks(list);
234 | }
235 | }
236 |
237 | @autobind
238 | @asyncActionCreator()
239 | @withApi()
240 | async loadTracks(list?: Playlist | Album, api?: Api): Promise {
241 | if (isAlbum(list)) {
242 | const tracks = await api!.albums.tracks.getAll(list);
243 | return {
244 | type: TRACKS_LOAD_ACTION,
245 | list,
246 | tracks
247 | };
248 |
249 | }
250 |
251 | if (!list || list.id === DUMMY_PLAYLIST.id) {
252 | return void 0;
253 | }
254 | const tracks = await api!.playlists.tracks.getAll(list);
255 | return {
256 | type: TRACKS_LOAD_ACTION,
257 | list,
258 | tracks
259 | };
260 | }
261 |
262 | @autobind
263 | @withErrorAsync()
264 | @withApi()
265 | async playTrack(offset: number, list: Playlist | Album, api?: Api): Promise {
266 | await api!.player.play.put({
267 | offset,
268 | albumUri: isAlbum(list) ? list.album.uri : list.uri
269 | });
270 | return;
271 | }
272 |
273 | @autobind
274 | @withErrorAsync()
275 | @withApi()
276 | async seekTo(time: string, api?: Api): Promise {
277 | const timeS = time || await window.showInputBox({ prompt: 'Select time to seek to in format mm:ss or ss' });
278 | if (!timeS) {
279 | return;
280 | }
281 | const invalidTimeFormatError = 'Invalid time format. Should be in format mm:ss or ss';
282 | const timeA = timeS.split(':');
283 | if (timeA.length > 2) {
284 | showErrorMessage(invalidTimeFormatError);
285 | return;
286 | }
287 | let minutes = 0;
288 | let seconds = 0;
289 | if (timeA[1]){
290 | minutes = parseFloat(timeA[0]);
291 | seconds = parseFloat(timeA[1]);
292 | } else {
293 | seconds = parseFloat(timeA[0]);
294 | }
295 |
296 | if (Number.isNaN(seconds) || Number.isNaN(minutes)){
297 | showErrorMessage(invalidTimeFormatError);
298 | return;
299 | }
300 |
301 | const seekTo = Math.round((minutes * 60 + seconds) * 1000);
302 |
303 | await api!.player.seek.put(seekTo);
304 | }
305 |
306 | @autobind
307 | actionSignIn() {
308 | commands.executeCommand('vscode.open', Uri.parse(`${getAuthServerUrl()}/login`)).then(() => {
309 | const { createServerPromise, dispose } = createDisposableAuthSever();
310 | createServerPromise.then(({ accessToken, refreshToken }) => {
311 | this._actionSignIn(accessToken, refreshToken);
312 | }).catch(e => {
313 | showInformationMessage(`Failed to retrieve access token : ${JSON.stringify(e)}`);
314 | }).then(() => {
315 | dispose();
316 | });
317 | });
318 | }
319 |
320 | @autobind
321 | @actionCreator()
322 | _actionSignIn(accessToken: string, refreshToken: string): SignInAction {
323 | return {
324 | accessToken,
325 | refreshToken,
326 | type: SIGN_IN_ACTION
327 | };
328 | }
329 |
330 | @autobind
331 | @actionCreator()
332 | actionSignOut(): SignOutAction {
333 | return {
334 | type: SIGN_OUT_ACTION
335 | };
336 | }
337 | }
338 |
339 | export const actionsCreator = new ActionCreator();
340 |
--------------------------------------------------------------------------------
/src/actions/common.ts:
--------------------------------------------------------------------------------
1 | import { Album, Playlist, Track } from '@vscodespotify/spotify-common/lib/spotify/consts';
2 | import { ISpotifyStatusState } from '../state/state';
3 |
4 | export const UPDATE_STATE_ACTION = 'UPDATE_STATE_ACTION' as 'UPDATE_STATE_ACTION';
5 | export const SIGN_IN_ACTION = 'SIGN_IN_ACTION' as 'SIGN_IN_ACTION';
6 | export const SIGN_OUT_ACTION = 'SIGN_OUT_ACTION' as 'SIGN_OUT_ACTION';
7 | export const PLAYLISTS_LOAD_ACTION = 'PLAYLISTS_LOAD_ACTION' as 'PLAYLISTS_LOAD_ACTION';
8 | export const ALBUM_LOAD_ACTION = 'ALBUM_LOAD_ACTION' as const;
9 | export const SELECT_PLAYLIST_ACTION = 'SELECT_PLAYLIST_ACTION' as 'SELECT_PLAYLIST_ACTION';
10 | export const SELECT_ALBUM_ACTION = 'SELECT_ALBUM_ACTION' as const;
11 | export const TRACKS_LOAD_ACTION = 'TRACKS_LOAD_ACTION' as 'TRACKS_LOAD_ACTION';
12 | export const SELECT_TRACK_ACTION = 'SELECT_TRACK_ACTION' as 'SELECT_TRACK_ACTION';
13 |
14 | export interface UpdateStateAction {
15 | type: typeof UPDATE_STATE_ACTION;
16 | state: Partial;
17 | }
18 |
19 | export interface SignInAction {
20 | type: typeof SIGN_IN_ACTION;
21 | accessToken: string;
22 | refreshToken: string;
23 | }
24 |
25 | export interface SignOutAction {
26 | type: typeof SIGN_OUT_ACTION;
27 | }
28 |
29 | export interface PlaylistsLoadAction {
30 | type: typeof PLAYLISTS_LOAD_ACTION;
31 | playlists: Playlist[];
32 | }
33 |
34 | export interface AlbumLoadAction {
35 | type: typeof ALBUM_LOAD_ACTION;
36 | albums: Album[];
37 | }
38 |
39 | export interface TracksLoadAction {
40 | type: typeof TRACKS_LOAD_ACTION;
41 | list: Playlist | Album;
42 | tracks: Track[];
43 | }
44 |
45 | export interface SelectPlaylistAction {
46 | type: typeof SELECT_PLAYLIST_ACTION;
47 | playlist: Playlist;
48 | }
49 |
50 | export interface SelectAlbumAction {
51 | type: typeof SELECT_ALBUM_ACTION;
52 | album: Album;
53 | }
54 |
55 | export interface SelectTrackAction {
56 | type: typeof SELECT_TRACK_ACTION;
57 | track: Track;
58 | }
59 |
60 | export type Action = UpdateStateAction |
61 | SignInAction |
62 | SignOutAction |
63 | PlaylistsLoadAction |
64 | AlbumLoadAction |
65 | SelectPlaylistAction |
66 | SelectAlbumAction |
67 | TracksLoadAction |
68 | SelectTrackAction;
69 |
--------------------------------------------------------------------------------
/src/auth/server/local.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import { Server } from 'http';
3 |
4 | import { getAuthServerUrl } from '../../config/spotify-config';
5 | import { log } from '../../info/info';
6 |
7 | export interface CreateDisposableAuthSeverPromiseResult {
8 | accessToken: string;
9 | refreshToken: string;
10 | }
11 |
12 | export function createDisposableAuthSever() {
13 | let server: Server;
14 | const createServerPromise = new Promise((res, rej) => {
15 | setTimeout(() => {
16 | rej('Timeout error. No response for 10 minutes.');
17 | }, 10 * 60 * 1000 /*10 minutes*/);
18 | try {
19 | const app = express();
20 |
21 | app.get('/callback', (request, response) => {
22 | const { access_token: accessToken, refresh_token: refreshToken, error } = request.query;
23 | if (!error) {
24 | res({ accessToken, refreshToken });
25 | } else {
26 | rej(error);
27 | }
28 | response.redirect(`${getAuthServerUrl()}/?message=${encodeURIComponent('You can now close this tab')}`);
29 | request.destroy();
30 | });
31 |
32 | server = app.listen(8350);
33 | } catch (e) {
34 | rej(e);
35 | }
36 | });
37 |
38 | return {
39 | createServerPromise,
40 | dispose: () => server && server.close(() => {
41 | log('server closed');
42 | })
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/commands.ts:
--------------------------------------------------------------------------------
1 | import { commands, Disposable } from 'vscode';
2 |
3 | import { actionsCreator } from './actions/actions';
4 | import { getTrackInfoClickBehaviour } from './config/spotify-config';
5 | import { LyricsController } from './lyrics/lyrics';
6 | import { SpotifyClient } from './spotify/common';
7 | import { Album, Playlist } from './state/state';
8 | import { SIGN_IN_COMMAND } from './consts/consts';
9 |
10 | export function createCommands(sC: SpotifyClient): { dispose: () => void } {
11 | const lC = new LyricsController();
12 |
13 | const lyrics = commands.registerCommand('spotify.lyrics', lC.findLyrics.bind(lC));
14 | const next = commands.registerCommand('spotify.next', sC.next.bind(sC));
15 | const previous = commands.registerCommand('spotify.previous', sC.previous.bind(sC));
16 | const play = commands.registerCommand('spotify.play', sC.play.bind(sC));
17 | const pause = commands.registerCommand('spotify.pause', sC.pause.bind(sC));
18 | const playPause = commands.registerCommand('spotify.playPause', sC.playPause.bind(sC));
19 | const muteVolume = commands.registerCommand('spotify.muteVolume', sC.muteVolume.bind(sC));
20 | const unmuteVolume = commands.registerCommand('spotify.unmuteVolume', sC.unmuteVolume.bind(sC));
21 | const muteUnmuteVolume = commands.registerCommand('spotify.muteUnmuteVolume', sC.muteUnmuteVolume.bind(sC));
22 | const volumeUp = commands.registerCommand('spotify.volumeUp', sC.volumeUp.bind(sC));
23 | const volumeDown = commands.registerCommand('spotify.volumeDown', sC.volumeDown.bind(sC));
24 | const toggleRepeating = commands.registerCommand('spotify.toggleRepeating', sC.toggleRepeating.bind(sC));
25 | const toggleShuffling = commands.registerCommand('spotify.toggleShuffling', sC.toggleShuffling.bind(sC));
26 | const signIn = commands.registerCommand(SIGN_IN_COMMAND, actionsCreator.actionSignIn);
27 | const signOut = commands.registerCommand('spotify.signOut', actionsCreator.actionSignOut);
28 | const loadPlaylists = commands.registerCommand('spotify.loadPlaylists', actionsCreator.loadPlaylists);
29 | const loadAlbums = commands.registerCommand('spotify.loadAlbums', actionsCreator.loadAlbums);
30 | const loadTracks = commands.registerCommand('spotify.loadTracks', actionsCreator.loadTracksForSelectedPlaylist);
31 | const trackInfoClick = commands.registerCommand('spotify.trackInfoClick', () => {
32 | const trackInfoClickBehaviour = getTrackInfoClickBehaviour();
33 | if (trackInfoClickBehaviour === 'focus_song') {
34 | actionsCreator.selectCurrentTrack();
35 | } else if (trackInfoClickBehaviour === 'play_pause') {
36 | sC.playPause();
37 | }
38 | });
39 | const playTrack = commands.registerCommand('spotify.playTrack', async (/*arguments from TrackTreeItem event*/offset: number, list: Playlist | Album) => {
40 | await actionsCreator.playTrack(offset, list);
41 | sC.queryStatusFunc();
42 | });
43 | const seekTo = commands.registerCommand('spotify.seekTo', (seekToMs: string) => {
44 | actionsCreator.seekTo(seekToMs);
45 | });
46 |
47 | return Disposable.from(lyrics,
48 | next,
49 | previous,
50 | play,
51 | pause,
52 | playPause,
53 | muteVolume,
54 | unmuteVolume,
55 | muteUnmuteVolume,
56 | volumeUp,
57 | volumeDown,
58 | toggleRepeating,
59 | toggleShuffling,
60 | signIn,
61 | signOut,
62 | loadPlaylists,
63 | loadAlbums,
64 | loadTracks,
65 | trackInfoClick,
66 | playTrack,
67 | seekTo,
68 | lC.registration
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/spotify-controls.ts:
--------------------------------------------------------------------------------
1 | import { extensions, StatusBarAlignment, StatusBarItem, window } from 'vscode';
2 |
3 | import { getButtonPriority, isButtonToBeShown } from '../config/spotify-config';
4 | import { BUTTON_ID_SIGN_IN, BUTTON_ID_SIGN_OUT } from '../consts/consts';
5 |
6 | export interface Button {
7 | /**
8 | * Id of button
9 | */
10 | id: string;
11 | /**
12 | * Text of button(Octicons)
13 | */
14 | text: string;
15 | /**
16 | * Generator of text for button(Octicons)
17 | */
18 | dynamicText?: (cond: boolean) => string;
19 | /**
20 | * Generator of color for button
21 | */
22 | dynamicColor?: (cond: boolean) => string;
23 | /**
24 | * Name of a button
25 | */
26 | buttonName: string;
27 | /**
28 | * Command that is executed when button is pressed
29 | */
30 | buttonCommand: string;
31 | /**
32 | * Priority of button (higher means closer to left side)
33 | */
34 | buttonPriority: number;
35 | /**
36 | * True if button is enabled in settings
37 | */
38 | visible: boolean;
39 | /**
40 | * vscode status bar item
41 | */
42 | statusBarItem: StatusBarItem;
43 | }
44 |
45 | export interface ButtonWithDynamicText extends Button {
46 | /**
47 | * Generator of text for button(Octicons)
48 | */
49 | dynamicText: (cond: boolean) => string;
50 | }
51 |
52 | export interface ButtonWithDynamicColor extends Button {
53 | /**
54 | * Generator of color for button
55 | */
56 | dynamicColor: (cond: boolean) => string;
57 | }
58 |
59 | export class SpotifyControls {
60 | get buttons(): Button[] {
61 | return this._buttons;
62 | }
63 |
64 | /**
65 | * All buttons of vscode-spotify
66 | */
67 | private _buttons: Button[];
68 | private _playPauseButton!: ButtonWithDynamicText;
69 | private _muteUnmuteVolumeButton!: ButtonWithDynamicText;
70 | private _toggleRepeatingButton!: ButtonWithDynamicColor;
71 | private _toggleShufflingButton!: ButtonWithDynamicColor;
72 | private _signInButton!: Button;
73 | private _signOutButton!: Button;
74 |
75 | constructor() {
76 | const buttonsInfo = [
77 | { id: 'next', text: '$(chevron-right)' },
78 | { id: 'previous', text: '$(chevron-left)' },
79 | { id: 'play', text: '$(triangle-right)' },
80 | { id: 'pause', text: '$(primitive-square)' },
81 | {
82 | id: 'playPause',
83 | text: '$(triangle-right)',
84 | dynamicText: (isPlaying?: boolean) => isPlaying ? '$(primitive-square)' : '$(triangle-right)'
85 | },
86 | { id: 'muteVolume', text: '$(mute)' },
87 | { id: 'unmuteVolume', text: '$(unmute)' },
88 | { id: 'muteUnmuteVolume', text: '$(mute)', dynamicText: (isMuted?: boolean) => isMuted ? '$(mute)' : '$(unmute)' },
89 | { id: 'volumeUp', text: '$(arrow-small-up)' },
90 | { id: 'volumeDown', text: '$(arrow-small-down)' },
91 | { id: 'toggleRepeating', text: '$(sync)', dynamicColor: (isRepeating?: boolean) => isRepeating ? 'white' : 'darkgrey' },
92 | { id: 'toggleShuffling', text: '$(git-branch)', dynamicColor: (isShuffling?: boolean) => isShuffling ? 'white' : 'darkgrey' },
93 | { id: 'lyrics', text: '$(book)' },
94 | { id: BUTTON_ID_SIGN_IN, text: '$(sign-in)' },
95 | { id: BUTTON_ID_SIGN_OUT, text: '$(sign-out)' }
96 | ];
97 | const extension = extensions.getExtension('shyykoserhiy.vscode-spotify');
98 | if (!extension) {
99 | this._buttons = [];
100 | return;
101 | }
102 | const commands: { command: string, title: string }[] = extension.packageJSON.contributes.commands;
103 | this._buttons = buttonsInfo.map(item => {
104 | const buttonName = item.id + 'Button';
105 | const buttonCommand = 'spotify.' + item.id;
106 | const buttonPriority = getButtonPriority(buttonName);
107 | const visible = isButtonToBeShown(buttonName);
108 | const statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, buttonPriority);
109 | const {title} = commands.filter(command => command.command === buttonCommand)[0] || { title: '' };
110 | statusBarItem.text = item.text;
111 | statusBarItem.command = buttonCommand;
112 | statusBarItem.tooltip = title;
113 |
114 | return Object.assign({}, item, { buttonName, buttonCommand, buttonPriority, statusBarItem, visible });
115 | });
116 | this._buttons.forEach(button => {
117 | if (button.id === 'playPause') {
118 | this._playPauseButton = button as ButtonWithDynamicText;
119 | return;
120 | }
121 | if (button.id === 'muteUnmuteVolume') {
122 | this._muteUnmuteVolumeButton = button as ButtonWithDynamicText;
123 | return;
124 | }
125 | if (button.id === 'toggleRepeating') {
126 | this._toggleRepeatingButton = button as ButtonWithDynamicColor;
127 | return;
128 | }
129 | if (button.id === 'toggleShuffling') {
130 | this._toggleShufflingButton = button as ButtonWithDynamicColor;
131 | return;
132 | }
133 | if (button.id === BUTTON_ID_SIGN_IN) {
134 | this._signInButton = button;
135 | return;
136 | }
137 | if (button.id === BUTTON_ID_SIGN_OUT) {
138 | this._signOutButton = button;
139 | }
140 | });
141 | }
142 |
143 | /**
144 | * Updates dynamicText buttons
145 | */
146 | updateDynamicButtons(playing: boolean, muted: boolean, repeating: boolean, shuffling: boolean): boolean {
147 | let changed = false;
148 | changed = this._updateText(this._playPauseButton, playing) || changed;
149 | changed = this._updateText(this._muteUnmuteVolumeButton, muted) || changed;
150 | changed = this._updateColor(this._toggleRepeatingButton, repeating) || changed;
151 | changed = this._updateColor(this._toggleShufflingButton, shuffling) || changed;
152 | return changed;
153 | }
154 |
155 | showHideAuthButtons() {
156 | this._hideShowButton(this._signInButton);
157 | this._hideShowButton(this._signOutButton);
158 | }
159 |
160 | /**
161 | * Show buttons that are visible
162 | */
163 | showVisible() {
164 | this.buttons.forEach(button => button.visible && button.statusBarItem.show());
165 | }
166 |
167 | /**
168 | * Hides all the buttons except auth buttons
169 | */
170 | hideAll() {
171 | this.buttons.forEach(button => {
172 | if (button === this._signInButton || button === this._signOutButton) {
173 | return;
174 | }
175 | button.statusBarItem.hide();
176 | });
177 | }
178 |
179 | /**
180 | * Disposes all the buttons
181 | */
182 | dispose() {
183 | this.buttons.forEach(button => { button.statusBarItem.dispose(); });
184 | }
185 | private _hideShowButton(button: Button) {
186 | button.visible = isButtonToBeShown(button.buttonName);
187 | button.visible ? button.statusBarItem.show() : button.statusBarItem.hide();
188 | }
189 |
190 | private _updateText(button: ButtonWithDynamicText, condition: boolean): boolean {
191 | if (!isButtonToBeShown(button.buttonName)) {
192 | return false;
193 | }
194 | const dynamicText = button.dynamicText(condition);
195 | if (dynamicText !== button.statusBarItem.text) {
196 | button.statusBarItem.text = dynamicText;
197 | return true;
198 | }
199 | return false;
200 | }
201 |
202 | private _updateColor(button: ButtonWithDynamicColor, condition: boolean): boolean {
203 | if (!isButtonToBeShown(button.buttonName)) {
204 | return false;
205 | }
206 | const dynamicColor = button.dynamicColor(condition);
207 | if (dynamicColor !== button.statusBarItem.color) {
208 | button.statusBarItem.color = dynamicColor;
209 | return true;
210 | }
211 | return false;
212 | }
213 | }
--------------------------------------------------------------------------------
/src/components/spotify-status.ts:
--------------------------------------------------------------------------------
1 | import { StatusBarAlignment, StatusBarItem, window } from 'vscode';
2 |
3 | import { getButtonPriority, getTrackInfoClickBehaviour, getTrackInfoFormat } from '../config/spotify-config';
4 | import { ILoginState, ITrack } from '../state/state';
5 | import { getState, getStore } from '../store/store';
6 |
7 | import { SpotifyControls } from './spotify-controls';
8 |
9 | export class SpotifyStatus {
10 | /**
11 | * Status bar with info from spotify
12 | */
13 | private _statusBarItem: StatusBarItem | undefined;
14 | private _spotifyControls: SpotifyControls | undefined;
15 | private loginState: ILoginState | null = null;
16 |
17 | constructor() {
18 | getStore().subscribe(() => {
19 | this.render();
20 | });
21 | }
22 |
23 | /**
24 | * Updates spotify status bar inside vscode
25 | */
26 | render() {
27 | const state = getState();
28 | // Create as needed
29 | if (!this._statusBarItem) {
30 | this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, getButtonPriority('trackInfo'));
31 | this._statusBarItem.show();
32 | }
33 | if (!this._spotifyControls) {
34 | this._spotifyControls = new SpotifyControls();
35 | this._spotifyControls.showVisible();
36 | }
37 | if (this.loginState !== state.loginState) {
38 | this.loginState = state.loginState;
39 | this._spotifyControls.showHideAuthButtons();
40 | }
41 |
42 | if (state.isRunning) {
43 | const { state: playing, volume, isRepeating, isShuffling } = state.playerState;
44 | const text = this.formattedTrackInfo(state.track);
45 | let toRedraw = false;
46 | if (text !== this._statusBarItem.text) {// we need this guard to prevent flickering
47 | this._statusBarItem.text = text;
48 | toRedraw = true;
49 | }
50 | if (this._spotifyControls.updateDynamicButtons(playing === 'playing', volume === 0, isRepeating, isShuffling)) {
51 | toRedraw = true;
52 | }
53 | if (toRedraw) {
54 | this._statusBarItem.show();
55 | this._spotifyControls.showVisible();
56 | }
57 | const trackInfoClickBehaviour = getTrackInfoClickBehaviour();
58 | if (trackInfoClickBehaviour === 'none') {
59 | this._statusBarItem.command = undefined;
60 | } else {
61 | this._statusBarItem.command = 'spotify.trackInfoClick';
62 | }
63 | } else {
64 | this._statusBarItem.text = '';
65 | this._statusBarItem.hide();
66 | this._spotifyControls.hideAll();
67 | }
68 | }
69 |
70 | /**
71 | * Disposes status bar items(if exist)
72 | */
73 | dispose() {
74 | if (this._statusBarItem) {
75 | this._statusBarItem.dispose();
76 | }
77 | if (this._spotifyControls) {
78 | this._spotifyControls.dispose();
79 | }
80 | }
81 |
82 | private formattedTrackInfo(track: ITrack): string {
83 | const { album, artist, name } = track;
84 | const keywordsMap: { [index: string]: string } = {
85 | albumName: album,
86 | artistName: artist,
87 | trackName: name
88 | };
89 |
90 | return getTrackInfoFormat().replace(/albumName|artistName|trackName/gi, matched => keywordsMap[matched]);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/tree-albums.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as vscode from 'vscode';
3 |
4 | import { actionsCreator } from '../actions/actions';
5 | import { isAlbum } from '../isAlbum';
6 | import { Album } from '../state/state';
7 | import { getState, getStore } from '../store/store';
8 |
9 | export const connectAlbumTreeView = (view: vscode.TreeView) =>
10 | vscode.Disposable.from(
11 | view.onDidChangeSelection(e => {
12 | actionsCreator.selectAlbumAction(e.selection[0]);
13 | actionsCreator.loadTracksIfNotLoaded(e.selection[0]);
14 | }),
15 | view.onDidChangeVisibility(e => {
16 | if (e.visible) {
17 | const state = getState();
18 | if (!state.albums.length) {
19 | actionsCreator.loadAlbums();
20 | }
21 |
22 | const playlistOrAlbum = state.selectedList;
23 | if (playlistOrAlbum && isAlbum(playlistOrAlbum)) {
24 | const p = state.albums.find(({album}) => album.id === playlistOrAlbum.album.id);
25 | if (p && !view.selection.indexOf(p)) {
26 | view.reveal(p, { focus: true, select: true });
27 | }
28 | }
29 | }
30 | })
31 | );
32 |
33 | export class TreeAlbumProvider implements vscode.TreeDataProvider {
34 | readonly onDidChangeTreeDataEmitter: vscode.EventEmitter = new vscode.EventEmitter();
35 | readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEmitter.event;
36 |
37 | private albums: Album[] = [];
38 |
39 | constructor() {
40 | getStore().subscribe(() => {
41 | const { albums } = getState();
42 |
43 | if (this.albums !== albums) {
44 | this.albums = albums;
45 | this.refresh();
46 | }
47 | });
48 | }
49 |
50 | getParent(_p: Album) {
51 | return void 0; // all albums are in root
52 | }
53 |
54 | refresh(): void {
55 | this.onDidChangeTreeDataEmitter.fire(void 0);
56 | }
57 |
58 | getTreeItem(p: Album): AlbumTreeItem {
59 | return new AlbumTreeItem(p, vscode.TreeItemCollapsibleState.None);
60 | }
61 |
62 | getChildren(element?: Album): Thenable {
63 | if (element) {
64 | return Promise.resolve([]);
65 | }
66 | if (!this.albums) {
67 | return Promise.resolve([]);
68 | }
69 |
70 | return new Promise(resolve => {
71 | resolve(this.albums);
72 | });
73 | }
74 | }
75 |
76 | class AlbumTreeItem extends vscode.TreeItem {
77 | // @ts-expect-error
78 | get tooltip(): string {
79 | return `${this.album.album.name} - ${this.album.album.artists.map(a => a.name).join(", ")}`;
80 | }
81 |
82 | iconPath = {
83 | light: path.join(__filename, '..', '..', '..', 'resources', 'light', 'playlist.svg'),
84 | dark: path.join(__filename, '..', '..', '..', 'resources', 'dark', 'playlist.svg')
85 | };
86 | contextValue = 'album';
87 |
88 | constructor(
89 | private readonly album: Album,
90 | readonly collapsibleState: vscode.TreeItemCollapsibleState,
91 | readonly command?: vscode.Command
92 | ) {
93 | super(album.album.name, collapsibleState);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/tree-playlists.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as vscode from 'vscode';
3 |
4 | import { actionsCreator } from '../actions/actions';
5 | import { isAlbum } from '../isAlbum';
6 | import { Playlist } from '../state/state';
7 | import { getState, getStore } from '../store/store';
8 |
9 | export const connectPlaylistTreeView = (view: vscode.TreeView) =>
10 | vscode.Disposable.from(
11 | view.onDidChangeSelection(e => {
12 | actionsCreator.selectPlaylistAction(e.selection[0]);
13 | actionsCreator.loadTracksIfNotLoaded(e.selection[0]);
14 | }),
15 | view.onDidChangeVisibility(e => {
16 | if (e.visible) {
17 | const state = getState();
18 | if (!state.playlists.length) {
19 | actionsCreator.loadPlaylists();
20 | }
21 |
22 | const playlistOrAlbum = state.selectedList;
23 | if (playlistOrAlbum && !isAlbum(playlistOrAlbum)) {
24 | const p = state.playlists.find(pl => pl.id === playlistOrAlbum.id);
25 | if (p && !view.selection.indexOf(p)) {
26 | view.reveal(p, { focus: true, select: true });
27 | }
28 | }
29 | }
30 | })
31 | );
32 |
33 | export class TreePlaylistProvider implements vscode.TreeDataProvider {
34 | readonly onDidChangeTreeDataEmitter: vscode.EventEmitter = new vscode.EventEmitter();
35 | readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEmitter.event;
36 |
37 | private playlists: Playlist[] = [];
38 |
39 | constructor() {
40 | getStore().subscribe(() => {
41 | const { playlists } = getState();
42 |
43 | if (this.playlists !== playlists) {
44 | this.playlists = playlists;
45 | this.refresh();
46 | }
47 | });
48 | }
49 |
50 | getParent(_p: Playlist) {
51 | return void 0; // all playlists are in root
52 | }
53 |
54 | refresh(): void {
55 | this.onDidChangeTreeDataEmitter.fire(void 0);
56 | }
57 |
58 | getTreeItem(p: Playlist): PlaylistTreeItem {
59 | return new PlaylistTreeItem(p, vscode.TreeItemCollapsibleState.None);
60 | }
61 |
62 | getChildren(element?: Playlist): Thenable {
63 | if (element) {
64 | return Promise.resolve([]);
65 | }
66 | if (!this.playlists) {
67 | return Promise.resolve([]);
68 | }
69 |
70 | return new Promise(resolve => {
71 | resolve(this.playlists);
72 | });
73 | }
74 | }
75 |
76 | class PlaylistTreeItem extends vscode.TreeItem {
77 | // @ts-expect-error
78 | get tooltip(): string {
79 | return `${this.playlist.name} by ${this.playlist.owner.display_name}`;
80 | }
81 |
82 | iconPath = {
83 | light: path.join(__filename, '..', '..', '..', 'resources', 'light', 'playlist.svg'),
84 | dark: path.join(__filename, '..', '..', '..', 'resources', 'dark', 'playlist.svg')
85 | };
86 | contextValue = 'playlist';
87 |
88 | constructor(
89 | private readonly playlist: Playlist,
90 | readonly collapsibleState: vscode.TreeItemCollapsibleState,
91 | readonly command?: vscode.Command
92 | ) {
93 | super(playlist.name, collapsibleState);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/tree-track.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as vscode from 'vscode';
3 |
4 | import { actionsCreator } from '../actions/actions';
5 | import { isAlbum } from '../isAlbum';
6 | import { Album, Playlist, Track } from '../state/state';
7 | import { getState, getStore } from '../store/store';
8 |
9 | const createTrackTreeItem = (t: Track, playlistOrAlbum: Playlist | Album, trackIndex: number) =>
10 | new TrackTreeItem(t, vscode.TreeItemCollapsibleState.None, {
11 | command: 'spotify.playTrack',
12 | title: 'Play track',
13 | arguments: [trackIndex, playlistOrAlbum]
14 | });
15 |
16 | export const connectTrackTreeView = (view: vscode.TreeView