diff --git a/README.md b/README.md index 0f28b43..73c3ae7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Playlist combiner Ever wanted to combine your spotify playlists into one big one? This Spicetify custom app is able to do just that. -The app currently allows combining playlist into one playlists and synchronizing them with the press of a button after adding songs to one of the source playlists. The ultimate goal is to add the songs from the source to the target automatically. But that's still w.i.p. + +### Features +* Combine multiple playlists into one +* Auto sync to add missing tracks when spotify starts ![combined playlists home page](docs/home.png) @@ -34,6 +37,3 @@ Playlists may also be added or removed via the edit modal. Removing a playlist d ``` ⚠️ Combining a large amount of songs into a playlist may take some time ``` - -## Todo -See the [issues](https://github.com/jeroentvb/spicetify-combined-playlists/issues) tab. diff --git a/package.json b/package.json index 774c910..79467f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "combined-playlists", - "version": "1.3.0", + "version": "1.4.0", "private": true, "scripts": { "build": "spicetify-creator", diff --git a/src/app.tsx b/src/app.tsx index 5851a3c..7ab9f4a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { combinePlaylists, getPlaylistInfo, getPaginatedSpotifyData, TrackState } from './utils'; +import { combinePlaylists, getPlaylistInfo, getPaginatedSpotifyData, TrackState, setCombinedPlaylistsSettings, getCombinedPlaylistsSettings } from './utils'; import { CREATE_NEW_PLAYLIST_IDENTIFIER, CREATE_PLAYLIST_URL, GET_PLAYLISTS_URL, LIKED_SONGS_PLAYLIST_FACADE, LS_KEY } from './constants'; -import type { CombinedPlaylist, SpotifyPlaylist, InitialPlaylistForm } from './types'; +import type { CombinedPlaylist, SpotifyPlaylist, InitialPlaylistForm, CombinedPlaylistsSettings } from './types'; import './assets/css/styles.scss'; import { SpicetifySvgIcon } from './components/SpicetifySvgIcon'; @@ -9,14 +9,19 @@ import { PlaylistForm } from './components/AddPlaylistForm'; import { AddPlaylistCard } from './components/AddPlaylistCard'; import { Card } from './components/Card'; import { ImportExportModal } from './components/ImportExportModal'; +import { synchronizeCombinedPlaylists } from './extensions/auto-sync'; export interface State { playlists: SpotifyPlaylist[]; combinedPlaylists: CombinedPlaylist[]; isLoading: boolean; isInitializing: boolean; + autoSync: boolean; } +// Needs to be deinfed to avoid eslint error +const SpotifyComponents = Spicetify.ReactComponent; + class App extends React.Component, State> { get combinedPlaylistsLs(): CombinedPlaylist[] { @@ -30,11 +35,14 @@ class App extends React.Component, State> { constructor(props: Record) { super(props); + const settings = getCombinedPlaylistsSettings(); + this.state = { playlists: [], combinedPlaylists: [], isLoading: false, isInitializing: false, + autoSync: settings.autoSync, }; } @@ -67,7 +75,11 @@ class App extends React.Component, State> { ? await this.createPlaylist(formData.sources) : this.findPlaylist(formData.target); - await combinePlaylists(sourcePlaylists, targetPlaylist); + await combinePlaylists(sourcePlaylists, targetPlaylist) + .catch((err) => { + console.error('An error ocurred while combining playlists', err); + Spicetify.showNotification('An error ocurred while combining playlists', true); + }); this.saveCombinedPlaylist(sourcePlaylists, targetPlaylist); Spicetify.PopupModal.hide(); @@ -120,7 +132,17 @@ class App extends React.Component, State> { const { sources } = this.state.combinedPlaylists.find((combinedPlaylist) => combinedPlaylist.target.id === playlistToSync.id) as CombinedPlaylist; const sourcePlaylists = sources.map((sourcePlaylist) => this.findPlaylist(sourcePlaylist.id)); - await combinePlaylists(sourcePlaylists, playlistToSync); + await combinePlaylists(sourcePlaylists, playlistToSync) + .catch((err) => { + console.error('An error ocurred while syncing playlists', err); + Spicetify.showNotification('An error ocurred while syncing playlists', true); + }); + } + + @TrackState('isLoading') + async syncAllPlaylists() { + Spicetify.showNotification('Synchronizing all combined playlists'); + await synchronizeCombinedPlaylists(); } findPlaylist(id: string): SpotifyPlaylist { @@ -182,13 +204,52 @@ class App extends React.Component, State> { }); } + toggleAutoSuync() { + const newSettings: CombinedPlaylistsSettings = { + ...getCombinedPlaylistsSettings(), + autoSync: !this.state.autoSync, + }; + + this.setState({ autoSync: newSettings.autoSync }); + setCombinedPlaylistsSettings(newSettings); + } + render() { + const menuWrapper = ( + } + onClick={() => this.openImportExportModal()} + > + Import / export + + } + onClick={() => this.toggleAutoSuync()} + > + {this.state.autoSync ? 'Disable auto sync' : 'Enable auto sync'} + + } + onClick={() => !this.state.isLoading && this.syncAllPlaylists()} + > + Synchronize all + + ); + return (

Playlist combiner

- + + +
{!this.state.isInitializing &&
diff --git a/src/constants/index.ts b/src/constants/index.ts index b912e40..52ddd99 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,16 +7,21 @@ export const GET_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists?limit= */ export const TRACKS_FROM_PLAYLIST_URL_FILTER = '?fields=items(track(uri)),next'; +export const GET_PLAYLIST_TRACKS_URL = (uri: string) => `sp://core-playlist/v1/playlist/${uri}/rows`; + +export const GET_LIKED_SONGS_LIST_URL = 'sp://core-collection/unstable/@/list/tracks/all?responseFormat=protobufJson'; + export const ADD_TRACKS_TO_PLAYLIST_URL = (id: string) => `https://api.spotify.com/v1/playlists/${id}/tracks`; export const LS_KEY = 'combined-playlists'; +export const LS_KEY_SETTINGS = 'combined-playlists-settings'; export const CREATE_PLAYLIST_URL = (userId: string) => `https://api.spotify.com/v1/users/${userId}/playlists`; export const CREATE_NEW_PLAYLIST_IDENTIFIER = 'CREATE_NEW_PLAYLIST_IDENTIFIER'; export const LIKED_SONGS_PLAYLIST_FACADE: SpotifyPlaylist = { - name: Spicetify.Platform.Translations['shared.library.entity-row.liked-songs.title'], + name: Spicetify.Platform?.Translations['shared.library.entity-row.liked-songs.title'], collaborative: false, description: '', external_urls: { spotify: '' }, @@ -39,5 +44,4 @@ export const LIKED_SONGS_PLAYLIST_FACADE: SpotifyPlaylist = { }, type: 'playlist', uri: 'spotify:playlist:liked-songs-facade' - }; diff --git a/src/extensions/auto-sync.tsx b/src/extensions/auto-sync.tsx new file mode 100644 index 0000000..da0a836 --- /dev/null +++ b/src/extensions/auto-sync.tsx @@ -0,0 +1,22 @@ +import { LS_KEY } from '../constants'; +import type { CombinedPlaylist } from '../types'; +import { combinePlaylists, getCombinedPlaylistsSettings } from '../utils'; + +(async () => { + while (!Spicetify?.Platform || !Spicetify?.CosmosAsync) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + const autoSync = getCombinedPlaylistsSettings().autoSync; + if (autoSync) synchronizeCombinedPlaylists(); +})(); + +export function synchronizeCombinedPlaylists() { + const combinedPlaylists: CombinedPlaylist[] = JSON.parse(Spicetify.LocalStorage.get(LS_KEY) as string) ?? []; + + return Promise.all(combinedPlaylists.map(({ sources, target }) => combinePlaylists(sources, target, true))) + .catch((err) => { + console.error('An error ocurred while auto-syncing playlists', err); + Spicetify.showNotification('An error ocurred while auto-syncing playlists', true); + }); +} diff --git a/src/types/combined-playlists-settings.ts b/src/types/combined-playlists-settings.ts new file mode 100644 index 0000000..225dd5f --- /dev/null +++ b/src/types/combined-playlists-settings.ts @@ -0,0 +1,3 @@ +export interface CombinedPlaylistsSettings { + autoSync: boolean; +} diff --git a/src/types/index.ts b/src/types/index.ts index 777860e..46bd793 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,3 +2,5 @@ export * from './combined-playlist'; export * from './initial-playlist-form'; export * from './spotify-playlist'; export * from './collection-call-response'; +export * from './playlist-tracks-response'; +export * from './combined-playlists-settings'; diff --git a/src/types/playlist-tracks-response.ts b/src/types/playlist-tracks-response.ts new file mode 100644 index 0000000..1950320 --- /dev/null +++ b/src/types/playlist-tracks-response.ts @@ -0,0 +1,5 @@ +export interface PlaylistRowsResponse { + rows: { + link: string + }[] +} diff --git a/src/types/spicetify.d.ts b/src/types/spicetify.d.ts index ad3e9e5..f5de49e 100644 --- a/src/types/spicetify.d.ts +++ b/src/types/spicetify.d.ts @@ -1,1357 +1,1945 @@ declare namespace Spicetify { - type Metadata = Partial>; - type ContextTrack = { - uri: string; - uid?: string; - metadata?: Metadata; - }; - type ProvidedTrack = ContextTrack & { - removed?: string[]; - blocked?: string[]; - provider?: string; - }; - interface ContextOption { - contextURI?: string; - index?: number; - trackUri?: string; - page?: number; - trackUid?: string; - sortedBy?: string; - filteredBy?: string; - shuffleContext?: boolean; - repeatContext?: boolean; - repeatTrack?: boolean; - offset?: number; - next_page_url?: string; - restrictions?: Record; - referrer?: string; - }; - type PlayerState = { - timestamp: number; - context_uri: string; - context_url: string; - context_restrictions: Record; - index?: { - page: number; - track: number; - }; - track?: ProvidedTrack; - playback_id?: string; - playback_quality?: string; - playback_speed?: number; - position_as_of_timestamp: number; - duration: number; - is_playing: boolean; - is_paused: boolean; - is_buffering: boolean; - play_origin: { - feature_identifier: string; - feature_version: string; - view_uri?: string; - external_referrer?: string; - referrer_identifier?: string; - device_identifier?: string; - }; - options: { - shuffling_context?: boolean; - repeating_context?: boolean; - repeating_track?: boolean; - }; - restrictions: Record; - suppressions: { - providers: string[]; - }; - debug: { - log: string[]; - }; - prev_tracks: ProvidedTrack[]; - next_tracks: ProvidedTrack[]; - context_metadata: Metadata; - page_metadata: Metadata; - session_id: string; - queue_revision: string; - }; - namespace Player { - /** - * Register a listener `type` on Spicetify.Player. - * - * On default, `Spicetify.Player` always dispatch: - * - `songchange` type when player changes track. - * - `onplaypause` type when player plays or pauses. - * - `onprogress` type when track progress changes. - * - `appchange` type when user changes page. - */ - function addEventListener(type: string, callback: (event?: Event) => void): void; - function addEventListener(type: "songchange", callback: (event?: Event & { data: PlayerState }) => void): void; - function addEventListener(type: "onplaypause", callback: (event?: Event & { data: PlayerState }) => void): void; - function addEventListener(type: "onprogress", callback: (event?: Event & { data: number }) => void): void; - function addEventListener(type: "appchange", callback: (event?: Event & { data: { - /** - * App href path - */ - path: string; - /** - * App container - */ - container: HTMLElement; - } }) => void): void; - /** - * Skip to previous track. - */ - function back(): void; - /** - * An object contains all information about current track and player. - */ - const data: PlayerState; - /** - * Decrease a small amount of volume. - */ - function decreaseVolume(): void; - /** - * Dispatches an event at `Spicetify.Player`. - * - * On default, `Spicetify.Player` always dispatch - * - `songchange` type when player changes track. - * - `onplaypause` type when player plays or pauses. - * - `onprogress` type when track progress changes. - * - `appchange` type when user changes page. - */ - function dispatchEvent(event: Event): void; - const eventListeners: { - [key: string]: Array<(event?: Event) => void> - }; - /** - * Convert milisecond to `mm:ss` format - * @param milisecond - */ - function formatTime(milisecond: number): string; - /** - * Return song total duration in milisecond. - */ - function getDuration(): number; - /** - * Return mute state - */ - function getMute(): boolean; - /** - * Return elapsed duration in milisecond. - */ - function getProgress(): number; - /** - * Return elapsed duration in percentage (0 to 1). - */ - function getProgressPercent(): number; - /** - * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). - */ - function getRepeat(): number; - /** - * Return current shuffle state. - */ - function getShuffle(): boolean; - /** - * Return track heart state. - */ - function getHeart(): boolean; - /** - * Return current volume level (0 to 1). - */ - function getVolume(): number; - /** - * Increase a small amount of volume. - */ - function increaseVolume(): void; - /** - * Return a boolean whether player is playing. - */ - function isPlaying(): boolean; - /** - * Skip to next track. - */ - function next(): void; - /** - * Pause track. - */ - function pause(): void; - /** - * Resume track. - */ - function play(): void; - /** - * Play a track, playlist, album, etc. immediately - * @param uri Spotify URI - * @param context - * @param options - */ - async function playUri(uri: string, context: any = {}, options: Options = {}); - /** - * Unregister added event listener `type`. - * @param type - * @param callback - */ - function removeEventListener(type: string, callback: (event?: Event) => void): void; - /** - * Seek track to position. - * @param position can be in percentage (0 to 1) or in milisecond. - */ - function seek(position: number): void; - /** - * Turn mute on/off - * @param state - */ - function setMute(state: boolean): void; - /** - * Change Repeat mode - * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. - */ - function setRepeat(mode: number): void; - /** - * Turn shuffle on/off. - * @param state - */ - function setShuffle(state: boolean): void; - /** - * Set volume level - * @param level 0 to 1 - */ - function setVolume(level: number): void; - /** - * Seek to previous `amount` of milisecond - * @param amount in milisecond. Default: 15000. - */ - function skipBack(amount?: number): void; - /** - * Seek to next `amount` of milisecond - * @param amount in milisecond. Default: 15000. - */ - function skipForward(amount?: number): void; - /** - * Toggle Heart (Favourite) track state. - */ - function toggleHeart(): void; - /** - * Toggle Mute/No mute. - */ - function toggleMute(): void; - /** - * Toggle Play/Pause. - */ - function togglePlay(): void; - /** - * Toggle No repeat/Repeat all/Repeat one. - */ - function toggleRepeat(): void; - /** - * Toggle Shuffle/No shuffle. - */ - function toggleShuffle(): void; - } - /** - * Adds a track/album or array of tracks/albums to prioritized queue. - */ - function addToQueue(uri: string | string[]): Promise; - /** - * @deprecated - */ - const BridgeAPI: any; - /** - * @deprecated - */ - const CosmosAPI: any; - /** - * Async wrappers of CosmosAPI - */ - namespace CosmosAsync { - type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB"; - interface Error { - code: number; - error: string; - message: string; - stack?: string; - } - - type Headers = Record; - type Body = Record; - - interface Response { - body: any; - headers: Headers; - status: number; - uri: string; - static isSuccessStatus(status: number): boolean; - } - - function head(url: string, headers?: Headers): Promise; - function get(url: string, body?: Body, headers?: Headers): Promise; - function post(url: string, body?: Body, headers?: Headers): Promise; - function put(url: string, body?: Body, headers?: Headers): Promise; - function del(url: string, body?: Body, headers?: Headers): Promise; - function patch(url: string, body?: Body, headers?: Headers): Promise; - function sub(url: string, callback: ((b: Response.body) => void), onError?: ((e: Error) => void), body?: Body, headers?: Headers): Promise; - function postSub(url: string, body?: Body, callback: ((b: Response.body) => void), onError?: ((e: Error) => void)): Promise; - function request(method: Method, url: string, body?: Body, headers?: Headers): Promise; - function resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise; - } - /** - * Fetch interesting colors from URI. - * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) - */ - function colorExtractor(uri: string): Promise<{ - DESATURATED: string; - LIGHT_VIBRANT: string; - PROMINENT: string; - VIBRANT: string; - VIBRANT_NON_ALARMING: string; - }>; - /** - * @deprecated - */ - function getAblumArtColors(): any; - /** - * Fetch track analyzed audio data. - * Beware, not all tracks have audio data. - * @param uri is optional. Leave it blank to get current track - * or specify another track uri. - */ - function getAudioData(uri?: string): Promise; - /** - * Set of APIs method to register, deregister hotkeys/shortcuts - */ - namespace Keyboard { - type ValidKey = "BACKSPACE" | "TAB" | "ENTER" | "SHIFT" | "CTRL" | "ALT" | "CAPS" | "ESCAPE" | "SPACE" | "PAGE_UP" | "PAGE_DOWN" | "END" | "HOME" | "ARROW_LEFT" | "ARROW_UP" | "ARROW_RIGHT" | "ARROW_DOWN" | "INSERT" | "DELETE" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "WINDOW_LEFT" | "WINDOW_RIGHT" | "SELECT" | "NUMPAD_0" | "NUMPAD_1" | "NUMPAD_2" | "NUMPAD_3" | "NUMPAD_4" | "NUMPAD_5" | "NUMPAD_6" | "NUMPAD_7" | "NUMPAD_8" | "NUMPAD_9" | "MULTIPLY" | "ADD" | "SUBTRACT" | "DECIMAL_POINT" | "DIVIDE" | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | "F11" | "F12" | ";" | "=" | " | " | "-" | "." | "/" | "`" | "[" | "\\" | "]" | "\"" | "~" | "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | ":" | "<" | ">" | "?" | "|"; - type KeysDefine = string | { - key: string; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; - meta?: boolean; - }; - const KEYS: Record; - function registerShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void); - function registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void); - function registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void); - function _deregisterShortcut(keys: KeysDefine); - function deregisterImportantShortcut(keys: KeysDefine); - }; - - /** - * @deprecated - */ - const LiveAPI: any; - - namespace LocalStorage { - /** - * Empties the list associated with the object of all key/value pairs, if there are any. - */ - function clear(): void; - /** - * Get key value - */ - function get(key: string): string | null; - /** - * Delete key - */ - function remove(key: string): void; - /** - * Set new value for key - */ - function set(key: string, value: string): void; - } - /** - * To create and prepend custom menu item in profile menu. - */ - namespace Menu { - /** - * Create a single toggle. - */ - class Item { - constructor(name: string, isEnabled: boolean, onClick: (self: Item) => void); - name: string; - isEnabled: boolean; - /** - * Change item name - */ - setName(name: string): void; - /** - * Change item enabled state. - * Visually, item would has a tick next to it if its state is enabled. - */ - setState(isEnabled: boolean): void; - /** - * Item is only available in Profile menu when method "register" is called. - */ - register(): void; - /** - * Stop item to be prepended into Profile menu. - */ - deregister(): void; - } - - /** - * Create a sub menu to contain Item toggles. - * `Item`s in `subItems` array shouldn't be registered. - */ - class SubMenu { - constructor(name: string, subItems: Item[]); - name: string; - /** - * Change SubMenu name - */ - setName(name: string): void; - /** - * Add an item to sub items list - */ - addItem(item: Item); - /** - * Remove an item from sub items list - */ - removeItem(item: Item); - /** - * SubMenu is only available in Profile menu when method "register" is called. - */ - register(): void; - /** - * Stop SubMenu to be prepended into Profile menu. - */ - deregister(): void; - } - } - - /** - * Keyboard shortcut library - * - * Documentation: https://craig.is/killing/mice v1.6.5 - * - * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, - * so new extension should use this library instead. - */ - function Mousetrap(element?: any): void; - - /** - * Contains vast array of internal APIs. - * Please explore in Devtool Console. - */ - const Platform: any; - /** - * Queue object contains list of queuing tracks, - * history of played tracks and current track metadata. - */ - const Queue: { - nextTracks: any[]; - prevTracks: any[]; - queueRevision: string; - track: any; - }; - /** - * Remove a track/album or array of tracks/albums from current queue. - */ - function removeFromQueue(uri: string | string[]): Promise; - /** - * Display a bubble of notification. Useful for a visual feedback. - */ - function showNotification(text: string): void; - /** - * Set of APIs method to parse and validate URIs. - */ - class URI { - constructor(type: string, props: any); - public type: string; - public id: string; - - /** - * Creates an application URI object from the current URI object. - * - * If the current URI object is already an application type, a copy is made. - * - * @return The current URI as an application URI. - */ - toAppType(): URI; - - /** - * Creates a URI object from an application URI object. - * - * If the current URI object is not an application type, a copy is made. - * - * @return The current URI as a real typed URI. - */ - toRealType(): URI; - - /** - * - * @return The URI representation of this uri. - */ - toURI(): string; - - /** - * - * @return The URI representation of this uri. - */ - toString(): string; - - /** - * Get the URL path of this uri. - * - * @param opt_leadingSlash True if a leading slash should be prepended. - * @return The path of this uri. - */ - toURLPath(opt_leadingSlash: boolean): string; - - /** - * - * @return The Play URL string for the uri. - */ - toPlayURL(): string; - - /** - * - * @return The URL string for the uri. - */ - toURL(): string; - - /** - * - * @return The Open URL string for the uri. - */ - toOpenURL(): string; - - /** - * - * @return The Play HTTPS URL string for the uri. - */ - toSecurePlayURL(): string; - - /** - * - * @return The HTTPS URL string for the uri. - */ - toSecureURL(): string; - - /** - * - * @return The Open HTTPS URL string for the uri. - */ - toSecureOpenURL(): string; - - /** - * - * @return The id of the uri as a bytestring. - */ - idToByteString(): string; - - getPath(): string; - - getBase62Id(): string; - - /** - * Checks whether two URI:s refer to the same thing even though they might - * not necessarily be equal. + type Icon = "album" | "artist" | "block" | "brightness" | "car" | "chart-down" | "chart-up" | "check" | "check-alt-fill" | "chevron-left" | "chevron-right" | "chromecast-disconnected" | "clock" | "collaborative" | "computer" | "copy" | "download" | "downloaded" | "edit" | "enhance" | "exclamation-circle" | "external-link" | "facebook" | "follow" | "fullscreen" | "gamepad" | "grid-view" | "heart" | "heart-active" | "instagram" | "laptop" | "library" | "list-view" | "location" | "locked" | "locked-active" | "lyrics" | "menu" | "minimize" | "minus" | "more" | "new-spotify-connect" | "offline" | "pause" | "phone" | "play" | "playlist" | "playlist-folder" | "plus-alt" | "plus2px" | "podcasts" | "projector" | "queue" | "repeat" | "repeat-once" | "search" | "search-active" | "shuffle" | "skip-back" | "skip-back15" | "skip-forward" | "skip-forward15" | "soundbetter" | "speaker" | "spotify" | "subtitles" | "tablet" | "ticket" | "twitter" | "visualizer" | "voice" | "volume" | "volume-off" | "volume-one-wave" | "volume-two-wave" | "watch" | "x"; + type Variant = "bass" | "forte" | "brio" | "altoBrio" | "alto" | "canon" | "celloCanon" | "cello" | "ballad" | "balladBold" | "viola" | "violaBold" | "mesto" | "mestoBold" | "metronome" | "finale" | "finaleBold" | "minuet" | "minuetBold"; + type SemanticColor = "textBase" | "textSubdued" | "textBrightAccent" | "textNegative" | "textWarning" | "textPositive" | "textAnnouncement" | "essentialBase" | "essentialSubdued" | "essentialBrightAccent" | "essentialNegative" | "essentialWarning" | "essentialPositive" | "essentialAnnouncement" | "decorativeBase" | "decorativeSubdued" | "backgroundBase" | "backgroundHighlight" | "backgroundPress" | "backgroundElevatedBase" | "backgroundElevatedHighlight" | "backgroundElevatedPress" | "backgroundTintedBase" | "backgroundTintedHighlight" | "backgroundTintedPress" | "backgroundUnsafeForSmallTextBase" | "backgroundUnsafeForSmallTextHighlight" | "backgroundUnsafeForSmallTextPress"; + type Metadata = Partial>; + type ContextTrack = { + uri: string; + uid?: string; + metadata?: Metadata; + }; + type ProvidedTrack = ContextTrack & { + removed?: string[]; + blocked?: string[]; + provider?: string; + }; + type ContextOption = { + contextURI?: string; + index?: number; + trackUri?: string; + page?: number; + trackUid?: string; + sortedBy?: string; + filteredBy?: string; + shuffleContext?: boolean; + repeatContext?: boolean; + repeatTrack?: boolean; + offset?: number; + next_page_url?: string; + restrictions?: Record; + referrer?: string; + }; + type PlayerState = { + timestamp: number; + context_uri: string; + context_url: string; + context_restrictions: Record; + index?: { + page: number; + track: number; + }; + track?: ProvidedTrack; + playback_id?: string; + playback_quality?: string; + playback_speed?: number; + position_as_of_timestamp: number; + duration: number; + is_playing: boolean; + is_paused: boolean; + is_buffering: boolean; + play_origin: { + feature_identifier: string; + feature_version: string; + view_uri?: string; + external_referrer?: string; + referrer_identifier?: string; + device_identifier?: string; + }; + options: { + shuffling_context?: boolean; + repeating_context?: boolean; + repeating_track?: boolean; + }; + restrictions: Record; + suppressions: { + providers: string[]; + }; + debug: { + log: string[]; + }; + prev_tracks: ProvidedTrack[]; + next_tracks: ProvidedTrack[]; + context_metadata: Metadata; + page_metadata: Metadata; + session_id: string; + queue_revision: string; + }; + namespace Player { + /** + * Register a listener `type` on Spicetify.Player. * - * These two Playlist URIs, for example, refer to the same playlist: + * On default, `Spicetify.Player` always dispatch: + * - `songchange` type when player changes track. + * - `onplaypause` type when player plays or pauses. + * - `onprogress` type when track progress changes. + * - `appchange` type when user changes page. + */ + function addEventListener(type: string, callback: (event?: Event) => void): void; + function addEventListener(type: "songchange", callback: (event?: Event & { data: PlayerState }) => void): void; + function addEventListener(type: "onplaypause", callback: (event?: Event & { data: PlayerState }) => void): void; + function addEventListener(type: "onprogress", callback: (event?: Event & { data: number }) => void): void; + function addEventListener(type: "appchange", callback: (event?: Event & { data: { + /** + * App href path + */ + path: string; + /** + * App container + */ + container: HTMLElement; + } }) => void): void; + /** + * Skip to previous track. + */ + function back(): void; + /** + * An object contains all information about current track and player. + */ + const data: PlayerState; + /** + * Decrease a small amount of volume. + */ + function decreaseVolume(): void; + /** + * Dispatches an event at `Spicetify.Player`. + * + * On default, `Spicetify.Player` always dispatch + * - `songchange` type when player changes track. + * - `onplaypause` type when player plays or pauses. + * - `onprogress` type when track progress changes. + * - `appchange` type when user changes page. + */ + function dispatchEvent(event: Event): void; + const eventListeners: { + [key: string]: Array<(event?: Event) => void> + }; + /** + * Convert milisecond to `mm:ss` format + * @param milisecond + */ + function formatTime(milisecond: number): string; + /** + * Return song total duration in milisecond. + */ + function getDuration(): number; + /** + * Return mute state + */ + function getMute(): boolean; + /** + * Return elapsed duration in milisecond. + */ + function getProgress(): number; + /** + * Return elapsed duration in percentage (0 to 1). + */ + function getProgressPercent(): number; + /** + * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). + */ + function getRepeat(): number; + /** + * Return current shuffle state. + */ + function getShuffle(): boolean; + /** + * Return track heart state. + */ + function getHeart(): boolean; + /** + * Return current volume level (0 to 1). + */ + function getVolume(): number; + /** + * Increase a small amount of volume. + */ + function increaseVolume(): void; + /** + * Return a boolean whether player is playing. + */ + function isPlaying(): boolean; + /** + * Skip to next track. + */ + function next(): void; + /** + * Pause track. + */ + function pause(): void; + /** + * Resume track. + */ + function play(): void; + /** + * Play a track, playlist, album, etc. immediately + * @param uri Spotify URI + * @param context + * @param options + */ + function playUri(uri: string, context?: any, options?: any): Promise; + /** + * Unregister added event listener `type`. + * @param type + * @param callback + */ + function removeEventListener(type: string, callback: (event?: Event) => void): void; + /** + * Seek track to position. + * @param position can be in percentage (0 to 1) or in milisecond. + */ + function seek(position: number): void; + /** + * Turn mute on/off + * @param state + */ + function setMute(state: boolean): void; + /** + * Change Repeat mode + * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. + */ + function setRepeat(mode: number): void; + /** + * Turn shuffle on/off. + * @param state + */ + function setShuffle(state: boolean): void; + /** + * Set volume level + * @param level 0 to 1 + */ + function setVolume(level: number): void; + /** + * Seek to previous `amount` of milisecond + * @param amount in milisecond. Default: 15000. + */ + function skipBack(amount?: number): void; + /** + * Seek to next `amount` of milisecond + * @param amount in milisecond. Default: 15000. + */ + function skipForward(amount?: number): void; + /** + * Toggle Heart (Favourite) track state. + */ + function toggleHeart(): void; + /** + * Toggle Mute/No mute. + */ + function toggleMute(): void; + /** + * Toggle Play/Pause. + */ + function togglePlay(): void; + /** + * Toggle No repeat/Repeat all/Repeat one. + */ + function toggleRepeat(): void; + /** + * Toggle Shuffle/No shuffle. + */ + function toggleShuffle(): void; + } + /** + * Adds a track or array of tracks to prioritized queue. + */ + function addToQueue(uri: ContextTrack[]): Promise; + /** + * @deprecated + */ + const BridgeAPI: any; + /** + * @deprecated + */ + const CosmosAPI: any; + /** + * Async wrappers of CosmosAPI + */ + namespace CosmosAsync { + type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB"; + interface Error { + code: number; + error: string; + message: string; + stack?: string; + } + + type Headers = Record; + type Body = Record; + + interface Response { + body: any; + headers: Headers; + status: number; + uri?: string; + } + + function head(url: string, headers?: Headers): Promise; + function get(url: string, body?: Body, headers?: Headers): Promise; + function post(url: string, body?: Body, headers?: Headers): Promise; + function put(url: string, body?: Body, headers?: Headers): Promise; + function del(url: string, body?: Body, headers?: Headers): Promise; + function patch(url: string, body?: Body, headers?: Headers): Promise; + function sub(url: string, callback: ((b: Response["body"]) => void), onError?: ((e: Error) => void), body?: Body, headers?: Headers): Promise; + function postSub(url: string, body: Body | null, callback: ((b: Response["body"]) => void), onError?: ((e: Error) => void)): Promise; + function request(method: Method, url: string, body?: Body, headers?: Headers): Promise; + function resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise; + } + /** + * Fetch interesting colors from URI. + * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) + */ + function colorExtractor(uri: string): Promise<{ + DESATURATED: string; + LIGHT_VIBRANT: string; + PROMINENT: string; + VIBRANT: string; + VIBRANT_NON_ALARMING: string; + }>; + /** + * @deprecated + */ + function getAblumArtColors(): any; + /** + * Fetch track analyzed audio data. + * Beware, not all tracks have audio data. + * @param uri is optional. Leave it blank to get current track + * or specify another track uri. + */ + function getAudioData(uri?: string): Promise; + /** + * Set of APIs method to register, deregister hotkeys/shortcuts + */ + namespace Keyboard { + type ValidKey = "BACKSPACE" | "TAB" | "ENTER" | "SHIFT" | "CTRL" | "ALT" | "CAPS" | "ESCAPE" | "SPACE" | "PAGE_UP" | "PAGE_DOWN" | "END" | "HOME" | "ARROW_LEFT" | "ARROW_UP" | "ARROW_RIGHT" | "ARROW_DOWN" | "INSERT" | "DELETE" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "WINDOW_LEFT" | "WINDOW_RIGHT" | "SELECT" | "NUMPAD_0" | "NUMPAD_1" | "NUMPAD_2" | "NUMPAD_3" | "NUMPAD_4" | "NUMPAD_5" | "NUMPAD_6" | "NUMPAD_7" | "NUMPAD_8" | "NUMPAD_9" | "MULTIPLY" | "ADD" | "SUBTRACT" | "DECIMAL_POINT" | "DIVIDE" | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | "F11" | "F12" | ";" | "=" | " | " | "-" | "." | "/" | "`" | "[" | "\\" | "]" | "\"" | "~" | "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | ":" | "<" | ">" | "?" | "|"; + type KeysDefine = string | { + key: string; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; + meta?: boolean; + }; + const KEYS: Record; + function registerShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; + function registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; + function registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; + function _deregisterShortcut(keys: KeysDefine): void; + function deregisterImportantShortcut(keys: KeysDefine): void; + function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; + } + + /** + * @deprecated + */ + const LiveAPI: any; + + namespace LocalStorage { + /** + * Empties the list associated with the object of all key/value pairs, if there are any. + */ + function clear(): void; + /** + * Get key value + */ + function get(key: string): string | null; + /** + * Delete key + */ + function remove(key: string): void; + /** + * Set new value for key + */ + function set(key: string, value: string): void; + } + /** + * To create and prepend custom menu item in profile menu. + */ + namespace Menu { + /** + * Create a single toggle. + */ + class Item { + constructor(name: string, isEnabled: boolean, onClick: (self: Item) => void, icon?: Icon | string); + name: string; + isEnabled: boolean; + /** + * Change item name + */ + setName(name: string): void; + /** + * Change item enabled state. + * Visually, item would has a tick next to it if its state is enabled. + */ + setState(isEnabled: boolean): void; + /** + * Change icon + */ + setIcon(icon: Icon | string): void; + /** + * Item is only available in Profile menu when method "register" is called. + */ + register(): void; + /** + * Stop item to be prepended into Profile menu. + */ + deregister(): void; + } + + /** + * Create a sub menu to contain Item toggles. + * `Item`s in `subItems` array shouldn't be registered. + */ + class SubMenu { + constructor(name: string, subItems: Item[]); + name: string; + /** + * Change SubMenu name + */ + setName(name: string): void; + /** + * Add an item to sub items list + */ + addItem(item: Item); + /** + * Remove an item from sub items list + */ + removeItem(item: Item); + /** + * SubMenu is only available in Profile menu when method "register" is called. + */ + register(): void; + /** + * Stop SubMenu to be prepended into Profile menu. + */ + deregister(): void; + } + } + + /** + * Keyboard shortcut library + * + * Documentation: https://craig.is/killing/mice v1.6.5 + * + * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, + * so new extension should use this library instead. + */ + function Mousetrap(element?: any): void; + + /** + * Contains vast array of internal APIs. + * Please explore in Devtool Console. + */ + const Platform: any; + /** + * Queue object contains list of queuing tracks, + * history of played tracks and current track metadata. + */ + const Queue: { + nextTracks: any[]; + prevTracks: any[]; + queueRevision: string; + track: any; + }; + /** + * Remove a track or array of tracks from current queue. + */ + function removeFromQueue(uri: ContextTrack[]): Promise; + /** + * Display a bubble of notification. Useful for a visual feedback. + * @param message Message to display. Can use inline HTML for styling. + * @param isError If true, bubble will be red. Defaults to false. + * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. + */ + function showNotification(message: React.ReactNode, isError?: boolean, msTimeout?: number): void; + /** + * Set of APIs method to parse and validate URIs. + */ + class URI { + constructor(type: string, props: any); + public type: string; + public hasBase62Id: boolean; + + public id?: string; + public disc?: any; + public args?: any; + public category?: string; + public username?: string; + public track?: string; + public artist?: string; + public album?: string; + public duration?: number; + public query?: string; + public country?: string; + public global?: boolean; + public context?: string | typeof URI | null; + public anchor?: string; + public play?: any; + public toplist?: any; + + /** + * + * @return The URI representation of this uri. + */ + toURI(): string; + + /** + * + * @return The URI representation of this uri. + */ + toString(): string; + + /** + * Get the URL path of this uri. + * + * @param opt_leadingSlash True if a leading slash should be prepended. + * @return The path of this uri. + */ + toURLPath(opt_leadingSlash: boolean): string; + + /** + * + * @param origin The origin to use for the URL. + * @return The URL string for the uri. + */ + toURL(origin?: string): string; + + /** + * Clones a given SpotifyURI instance. + * + * @return An instance of URI. + */ + clone(): URI | null; + + /** + * Gets the path of the URI object by removing all hash and query parameters. + * + * @return The path of the URI object. + */ + getPath(): string; + + /** + * The various URI Types. + * + * Note that some of the types in this enum are not real URI types, but are + * actually URI particles. They are marked so. + * + */ + static Type: { + AD: string; + ALBUM: string; + GENRE: string; + QUEUE: string; + APPLICATION: string; + ARTIST: string; + ARTIST_TOPLIST: string; + ARTIST_CONCERTS: string; + AUDIO_FILE: string; + COLLECTION: string; + COLLECTION_ALBUM: string; + COLLECTION_ARTIST: string; + COLLECTION_MISSING_ALBUM: string; + COLLECTION_TRACK_LIST: string; + CONCERT: string; + CONTEXT_GROUP: string; + DAILY_MIX: string; + EMPTY: string; + EPISODE: string; + /** URI particle; not an actual URI. */ + FACEBOOK: string; + FOLDER: string; + FOLLOWERS: string; + FOLLOWING: string; + IMAGE: string; + INBOX: string; + INTERRUPTION: string; + LIBRARY: string; + LIVE: string; + ROOM: string; + EXPRESSION: string; + LOCAL: string; + LOCAL_TRACK: string; + LOCAL_ALBUM: string; + LOCAL_ARTIST: string; + MERCH: string; + MOSAIC: string; + PLAYLIST: string; + PLAYLIST_V2: string; + PRERELEASE: string; + PROFILE: string; + PUBLISHED_ROOTLIST: string; + RADIO: string; + ROOTLIST: string; + SEARCH: string; + SHOW: string; + SOCIAL_SESSION: string; + SPECIAL: string; + STARRED: string; + STATION: string; + TEMP_PLAYLIST: string; + TOPLIST: string; + TRACK: string; + TRACKSET: string; + USER_TOPLIST: string; + USER_TOP_TRACKS: string; + UNKNOWN: string; + MEDIA: string; + QUESTION: string; + POLL: string; + }; + + /** + * Creates a new URI object from a parsed string argument. + * + * @param str The string that will be parsed into a URI object. + * @throws TypeError If the string argument is not a valid URI, a TypeError will + * be thrown. + * @return The parsed URI object. + */ + static fromString(str: string): URI; + + /** + * Parses a given object into a URI instance. + * + * Unlike URI.fromString, this function could receive any kind of value. If + * the value is already a URI instance, it is simply returned. + * Otherwise the value will be stringified before parsing. + * + * This function also does not throw an error like URI.fromString, but + * instead simply returns null if it can't parse the value. + * + * @param value The value to parse. + * @return The corresponding URI instance, or null if the + * passed value is not a valid value. + */ + static from(value: any): URI | null; + + /** + * Checks whether two URI:s refer to the same thing even though they might + * not necessarily be equal. + * + * These two Playlist URIs, for example, refer to the same playlist: + * + * spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2 + * spotify:playlist:3vxotOnOGDlZXyzJPLFnm2 + * + * @param baseUri The first URI to compare. + * @param refUri The second URI to compare. + * @return Whether they shared idenitity + */ + static isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean; + + /** + * Returns the hex representation of a Base62 encoded id. + * + * @param id The base62 encoded id. + * @return The hex representation of the base62 id. + */ + static idToHex(id: string): string; + + /** + * Returns the base62 representation of a hex encoded id. + * + * @param hex The hex encoded id. + * @return The base62 representation of the id. + */ + static hexToId(hex: string): string; + + /** + * Creates a new 'album' type URI. + * + * @param id The id of the album. + * @param disc The disc number of the album. + * @return The album URI. + */ + static albumURI(id: string, disc: number): URI; + + /** + * Creates a new 'application' type URI. + * + * @param id The id of the application. + * @param args An array containing the arguments to the app. + * @return The application URI. + */ + static applicationURI(id: string, args: string[]): URI; + + /** + * Creates a new 'artist' type URI. + * + * @param id The id of the artist. + * @return The artist URI. + */ + static artistURI(id: string): URI; + + /** + * Creates a new 'collection' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param category The category of the collection. + * @return The collection URI. + */ + static collectionURI(username: string, category: string): URI; + + /** + * Creates a new 'collection-album' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param id The id of the album. + * @return The collection album URI. + */ + static collectionAlbumURI(username: string, id: string): URI; + + /** + * Creates a new 'collection-artist' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param id The id of the artist. + * @return The collection artist URI. + */ + static collectionAlbumURI(username: string, id: string): URI; + + /** + * Creates a new 'concert' type URI. + * + * @param id The id of the concert. + * @return The concert URI. + */ + static concertURI(id: string): URI; + + /** + * Creates a new 'episode' type URI. + * + * @param id The id of the episode. + * @return The episode URI. + */ + static episodeURI(id: string): URI; + + /** + * Creates a new 'folder' type URI. + * + * @param id The id of the folder. + * @return The folder URI. + */ + static folderURI(id: string): URI; + + /** + * Creates a new 'local-album' type URI. + * + * @param artist The artist of the album. + * @param album The name of the album. + * @return The local album URI. + */ + static localAlbumURI(artist: string, album: string): URI; + + /** + * Creates a new 'local-artist' type URI. + * + * @param artist The name of the artist. + * @return The local artist URI. + */ + static localArtistURI(artist: string): URI; + + /** + * Creates a new 'playlist-v2' type URI. + * + * @param id The id of the playlist. + * @return The playlist URI. + */ + static playlistV2URI(id: string): URI; + + /** + * Creates a new 'prerelease' type URI. + * + * @param id The id of the prerelease. + * @return The prerelease URI. + */ + static prereleaseURI(id: string): URI; + + /** + * Creates a new 'profile' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param args A list of arguments. + * @return The profile URI. + */ + static profileURI(username: string, args: string[]): URI; + + /** + * Creates a new 'search' type URI. + * + * @param query The unencoded search query. + * @return The search URI + */ + static searchURI(query: string): URI; + + /** + * Creates a new 'show' type URI. + * + * @param id The id of the show. + * @return The show URI. + */ + static showURI(id: string): URI; + + /** + * Creates a new 'station' type URI. + * + * @param args An array of arguments for the station. + * @return The station URI. + */ + static stationURI(args: string[]): URI; + + /** + * Creates a new 'track' type URI. + * + * @param id The id of the track. + * @param anchor The point in the track formatted as mm:ss + * @param context An optional context URI + * @param play Toggles autoplay + * @return The track URI. + */ + static trackURI(id: string, anchor: string, context?: string, play: boolean): URI; + + /** + * Creates a new 'user-toplist' type URI. + * + * @param username The non-canonical username of the toplist owner. + * @param toplist The toplist type. + * @return The user-toplist URI. + */ + static userToplistURI(username: string, toplist: string): URI; + + static isAd(uri: URI | string): boolean; + static isAlbum(uri: URI | string): boolean; + static isGenre(uri: URI | string): boolean; + static isQueue(uri: URI | string): boolean; + static isApplication(uri: URI | string): boolean; + static isArtist(uri: URI | string): boolean; + static isArtistToplist(uri: URI | string): boolean; + static isArtistConcerts(uri: URI | string): boolean; + static isAudioFile(uri: URI | string): boolean; + static isCollection(uri: URI | string): boolean; + static isCollectionAlbum(uri: URI | string): boolean; + static isCollectionArtist(uri: URI | string): boolean; + static isCollectionMissingAlbum(uri: URI | string): boolean; + static isCollectionTrackList(uri: URI | string): boolean; + static isConcert(uri: URI | string): boolean; + static isContextGroup(uri: URI | string): boolean; + static isDailyMix(uri: URI | string): boolean; + static isEmpty(uri: URI | string): boolean; + static isEpisode(uri: URI | string): boolean; + static isFacebook(uri: URI | string): boolean; + static isFolder(uri: URI | string): boolean; + static isFollowers(uri: URI | string): boolean; + static isFollowing(uri: URI | string): boolean; + static isImage(uri: URI | string): boolean; + static isInbox(uri: URI | string): boolean; + static isInterruption(uri: URI | string): boolean; + static isLibrary(uri: URI | string): boolean; + static isLive(uri: URI | string): boolean; + static isRoom(uri: URI | string): boolean; + static isExpression(uri: URI | string): boolean; + static isLocal(uri: URI | string): boolean; + static isLocalTrack(uri: URI | string): boolean; + static isLocalAlbum(uri: URI | string): boolean; + static isLocalArtist(uri: URI | string): boolean; + static isMerch(uri: URI | string): boolean; + static isMosaic(uri: URI | string): boolean; + static isPlaylist(uri: URI | string): boolean; + static isPlaylistV2(uri: URI | string): boolean; + static isPrerelease(uri: URI | string): boolean; + static isProfile(uri: URI | string): boolean; + static isPublishedRootlist(uri: URI | string): boolean; + static isRadio(uri: URI | string): boolean; + static isRootlist(uri: URI | string): boolean; + static isSearch(uri: URI | string): boolean; + static isShow(uri: URI | string): boolean; + static isSocialSession(uri: URI | string): boolean; + static isSpecial(uri: URI | string): boolean; + static isStarred(uri: URI | string): boolean; + static isStation(uri: URI | string): boolean; + static isTempPlaylist(uri: URI | string): boolean; + static isToplist(uri: URI | string): boolean; + static isTrack(uri: URI | string): boolean; + static isTrackset(uri: URI | string): boolean; + static isUserToplist(uri: URI | string): boolean; + static isUserTopTracks(uri: URI | string): boolean; + static isUnknown(uri: URI | string): boolean; + static isMedia(uri: URI | string): boolean; + static isQuestion(uri: URI | string): boolean; + static isPoll(uri: URI | string): boolean; + static isPlaylistV1OrV2(uri: URI | string): boolean; + } + + /** + * Create custom menu item and prepend to right click context menu + */ + namespace ContextMenu { + type OnClickCallback = (uris: string[], uids?: string[], contextUri?: string) => void; + type ShouldAddCallback = (uris: string[], uids?: string[], contextUri?: string) => boolean; + + // Single context menu item + class Item { + /** + * List of valid icons to use. + */ + static readonly iconList: Icon[]; + constructor(name: string, onClick: OnClickCallback, shouldAdd?: ShouldAddCallback, icon?: Icon, disabled?: boolean); + name: string; + icon: Icon | string; + disabled: boolean; + /** + * A function returning boolean determines whether item should be prepended. + */ + shouldAdd: ShouldAddCallback; + /** + * A function to call when item is clicked + */ + onClick: OnClickCallback; + /** + * Item is only available in Context Menu when method "register" is called. + */ + register: () => void; + /** + * Stop Item to be prepended into Context Menu. + */ + deregister: () => void; + } + + /** + * Create a sub menu to contain `Item`s. + * `Item`s in `subItems` array shouldn't be registered. + */ + class SubMenu { + constructor(name: string, subItems: Iterable, shouldAdd?: ShouldAddCallback, disabled?: boolean); + name: string; + disabled: boolean; + /** + * A function returning boolean determines whether item should be prepended. + */ + shouldAdd: ShouldAddCallback; + addItem: (item: Item) => void; + removeItem: (item: Item) => void; + /** + * SubMenu is only available in Context Menu when method "register" is called. + */ + register: () => void; + /** + * Stop SubMenu to be prepended into Context Menu. + */ + deregister: () => void; + } + } + + /** + * Popup Modal + */ + namespace PopupModal { + interface Content { + title: string; + /** + * You can specify a string for simple text display + * or a HTML element for interactive config/setting menu + */ + content: string | Element | JSX.Element; + /** + * Bigger window + */ + isLarge?: boolean; + } + + function display(e: Content): void; + function hide(): void; + } + + /** React instance to create components */ + const React: any; + /** React DOM instance to render and mount components */ + const ReactDOM: any; + /** React DOM Server instance to render components to string */ + const ReactDOMServer: any; + + /** Stock React components exposed from Spotify library */ + namespace ReactComponent { + type ContextMenuProps = { + /** + * Decide whether to use the global singleton context menu (rendered in ) + * or a new inline context menu (rendered in a sibling + * element to `children`) + */ + renderInline?: boolean; + /** + * Determins what will trigger the context menu. For example, a click, or a right-click + */ + trigger?: 'click' | 'right-click'; + /** + * Determins is the context menu should open or toggle when triggered + */ + action?: 'toggle' | 'open'; + /** + * The preferred placement of the context menu when it opens. + * Relative to trigger element. + */ + placement?: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'; + /** + * The x and y offset distances at which the context menu should open. + * Relative to trigger element and `position`. + */ + offset?: [number, number]; + /** + * Will stop the client from scrolling while the context menu is open + */ + preventScrollingWhileOpen?: boolean; + /** + * The menu UI to render inside of the context menu. + */ + menu: typeof Spicetify.ReactComponent.Menu | + typeof Spicetify.ReactComponent.AlbumMenu | + typeof Spicetify.ReactComponent.PodcastShowMenu | + typeof Spicetify.ReactComponent.ArtistMenu | + typeof Spicetify.ReactComponent.PlaylistMenu; + /** + * A child of the context menu. Should be `