diff --git a/.config/spicetify/CustomApps/library/collections_wrapper.js b/.config/spicetify/CustomApps/library/collections_wrapper.js index a4175d9d..dd3e1c01 100644 --- a/.config/spicetify/CustomApps/library/collections_wrapper.js +++ b/.config/spicetify/CustomApps/library/collections_wrapper.js @@ -8,6 +8,7 @@ var library = (() => { var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); @@ -21,6 +22,10 @@ var library = (() => { return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; + }; // src/extensions/collections_wrapper.ts var collections_wrapper_exports = {}; @@ -77,7 +82,7 @@ var library = (() => { var v4_default = v4; // src/extensions/collections_wrapper.ts - var CollectionWrapper = class extends EventTarget { + var _CollectionsWrapper = class extends EventTarget { _collections; constructor() { super(); @@ -90,115 +95,189 @@ var library = (() => { getCollection(uri) { return this._collections.find((collection) => collection.uri === uri); } - async requestAlbums({ sortOrder, textFilter }) { + async getCollectionContents(uri) { + const collection = this.getCollection(uri); + if (!collection) + throw new Error("Collection not found"); + const items = this._collections.filter((collection2) => collection2.parentCollection === uri); const albums = await Spicetify.Platform.LibraryAPI.getContents({ filters: ["0"], - sortOrder, - textFilter, offset: 0, limit: 9999 }); - return albums; + items.push(...albums.items.filter((album) => collection.items.includes(album.uri))); + return items; } - async getCollectionItems(props) { - const { collectionUri, textFilter, sortOrder, rootlist, limit = 9999, offset = 0 } = props; - let collectionItems = this._collections; - let albumItems = []; - let unfilteredLength = this._collections.length; - let openedCollection = ""; - if (collectionUri) { - const collection = this.getCollection(collectionUri); - const res = await this.requestAlbums({ sortOrder, textFilter }); - const collectionSet = new Set(collection.items); - const commonElements = res.items.filter((item) => collectionSet.has(item.uri)); - const collections = this._collections.filter((collection2) => collection2.parentCollection === collectionUri); - openedCollection = collection.name; - collectionItems = collections; - albumItems = commonElements; - unfilteredLength = collection.totalLength; - } + async getContents(props) { + const { collectionUri, offset, limit, textFilter } = props; + let items = collectionUri ? await this.getCollectionContents(collectionUri) : this._collections; + const openedCollectionName = collectionUri ? this.getCollection(collectionUri)?.name : void 0; if (textFilter) { - let regex = new RegExp("\\b" + textFilter, "i"); - collectionItems = collectionItems.filter((item) => { - return regex.test(item.name); - }); + const regex = new RegExp(`\\b${textFilter}`, "i"); + items = items.filter((collection) => regex.test(collection.name)); } - if (rootlist && !collectionUri) { - const res = await this.requestAlbums({ sortOrder, textFilter }); - albumItems = res.items; - if (!textFilter) { - const collectionSet = new Set(this._collections.map((collection) => collection.items).flat()); - const uncommonElements = res.items.filter((item) => !collectionSet.has(item.uri)); - collectionItems = this._collections.filter((collection) => !collection.parentCollection); - albumItems = uncommonElements; - unfilteredLength = this._collections.length + uncommonElements.length; + items = items.slice(offset, offset + limit); + return { items, totalLength: this._collections.length, offset, openedCollectionName }; + } + async cleanCollections() { + for (const collection of this._collections) { + const boolArray = await Spicetify.Platform.LibraryAPI.contains(...collection.items); + if (boolArray.includes(false)) { + collection.items = collection.items.filter((_, i) => boolArray[i]); + this.saveCollections(); + Spicetify.showNotification("Album removed from collection"); + this.syncCollection(collection.uri); } } - if (offset > 0) - collectionItems = []; - return { - openedCollection, - items: [...collectionItems, ...albumItems.slice(offset, offset + limit)], - totalLength: albumItems.length + collectionItems.length, - unfilteredLength - }; + } + async syncCollection(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + const { PlaylistAPI } = Spicetify.Platform; + if (!collection.syncedPlaylistUri) + return; + const playlist = await PlaylistAPI.getPlaylist(collection.syncedPlaylistUri); + const playlistTracks = playlist.contents.items.filter((t) => t.type === "track").map((t) => t.uri); + const collectionTracks = await this.getTracklist(uri); + const wanted = collectionTracks.filter((track) => !playlistTracks.includes(track)); + const unwanted = playlistTracks.filter((track) => !collectionTracks.includes(track)).map((uri2) => ({ uri: uri2, uid: [] })); + if (wanted.length) + await PlaylistAPI.add(collection.syncedPlaylistUri, wanted, { before: "end" }); + if (unwanted.length) + await PlaylistAPI.remove(collection.syncedPlaylistUri, unwanted); + } + unsyncCollection(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + collection.syncedPlaylistUri = void 0; + this.saveCollections(); + } + async getTracklist(collectionUri) { + const collection = this.getCollection(collectionUri); + if (!collection) + return []; + return Promise.all( + collection.items.map(async (uri) => { + const album = await Spicetify.Platform.LibraryAPI.getAlbum(uri); + return album.items.map((t) => t.uri); + }) + ).then((tracks) => tracks.flat()); + } + async convertToPlaylist(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + const { Platform, showNotification } = Spicetify; + const { RootlistAPI, PlaylistAPI } = Platform; + if (collection.syncedPlaylistUri) { + showNotification("Synced Playlist already exists", true); + return; + } + try { + const playlistUri = await RootlistAPI.createPlaylist(collection.name, { before: "start" }); + const items = await this.getTracklist(uri); + await PlaylistAPI.add(playlistUri, items, { before: "start" }); + collection.syncedPlaylistUri = playlistUri; + } catch (error) { + console.error(error); + showNotification("Failed to create playlist", true); + } + } + async createCollectionFromDiscog(artistUri) { + const [raw, info] = await Promise.all([ + Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryArtistDiscographyAlbums, { + uri: artistUri, + offset: 0, + limit: 50 + }), + Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryArtistOverview, { + uri: artistUri, + locale: Spicetify.Locale.getLocale(), + includePrerelease: false + }) + ]); + const items = raw?.data?.artistUnion.discography.albums?.items; + const name = info?.data?.artistUnion.profile.name; + const image = info?.data?.artistUnion.visuals.avatarImage?.sources?.[0]?.url; + if (!name || !items?.length) { + Spicetify.showNotification("Artist not found or has no albums"); + return; + } + const collectionUri = this.createCollection(`${name} Albums`); + if (image) + this.setCollectionImage(collectionUri, image); + for (const album of items) { + this.addAlbumToCollection(collectionUri, album.releases.items[0].uri); + } } createCollection(name, parentCollection = "") { - const uri = v4_default(); - const collection = { + const id = v4_default(); + this._collections.push({ type: "collection", - uri, + uri: id, name, items: [], - totalLength: 0, - imgUrl: "", + addedAt: new Date(), + lastPlayedAt: new Date(), parentCollection - }; - this._collections.push(collection); + }); this.saveCollections(); Spicetify.showNotification("Collection created"); + return id; } deleteCollection(uri) { this._collections = this._collections.filter((collection) => collection.uri !== uri); this.saveCollections(); Spicetify.showNotification("Collection deleted"); } + deleteCollectionAndAlbums(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + for (const album of collection.items) { + Spicetify.Platform.LibraryAPI.remove({ uris: [album] }); + } + this.deleteCollection(uri); + } async addAlbumToCollection(collectionUri, albumUri) { const collection = this.getCollection(collectionUri); if (!collection) return; + await Spicetify.Platform.LibraryAPI.add({ uris: [albumUri] }); collection.items.push(albumUri); - collection.totalLength++; this.saveCollections(); Spicetify.showNotification("Album added to collection"); + this.syncCollection(collectionUri); } removeAlbumFromCollection(collectionUri, albumUri) { const collection = this.getCollection(collectionUri); if (!collection) return; collection.items = collection.items.filter((item) => item !== albumUri); - collection.totalLength--; this.saveCollections(); Spicetify.showNotification("Album removed from collection"); + this.syncCollection(collectionUri); } getCollectionsWithAlbum(albumUri) { return this._collections.filter((collection) => { return collection.items.some((item) => item === albumUri); }); } - renameCollection(uri, newName) { + renameCollection(uri, name) { const collection = this.getCollection(uri); if (!collection) return; - collection.name = newName; + collection.name = name; this.saveCollections(); Spicetify.showNotification("Collection renamed"); } - setCollectionImage(uri, imgUrl) { + setCollectionImage(uri, url) { const collection = this.getCollection(uri); if (!collection) return; - collection.imgUrl = imgUrl; + collection.image = url; this.saveCollections(); Spicetify.showNotification("Collection image set"); } @@ -206,12 +285,15 @@ var library = (() => { const collection = this.getCollection(uri); if (!collection) return; - collection.imgUrl = ""; + collection.image = void 0; this.saveCollections(); Spicetify.showNotification("Collection image removed"); } }; - var collections_wrapper_default = CollectionWrapper; + var CollectionsWrapper = _CollectionsWrapper; + __publicField(CollectionsWrapper, "INSTANCE", new _CollectionsWrapper()); + window.CollectionsWrapper = CollectionsWrapper.INSTANCE; + var collections_wrapper_default = CollectionsWrapper; return __toCommonJS(collections_wrapper_exports); })(); diff --git a/.config/spicetify/CustomApps/library/extension.js b/.config/spicetify/CustomApps/library/extension.js index 55f750cc..7de5c55c 100644 --- a/.config/spicetify/CustomApps/library/extension.js +++ b/.config/spicetify/CustomApps/library/extension.js @@ -285,7 +285,9 @@ var library = (() => { }); }; var ToggleFiltersButton = () => { - const [direction, setDirection] = import_react3.default.useState(document.body.classList.contains("show-ylx-filters") ? "up" : "down"); + const [direction, setDirection] = import_react3.default.useState( + document.body.classList.contains("show-ylx-filters") ? "up" : "down" + ); const { ButtonTertiary } = Spicetify.ReactComponent; const toggleDirection = () => { if (direction === "down") { @@ -332,31 +334,207 @@ var library = (() => { }; var collapse_button_default = CollapseButton; - // src/components/expand_button.tsx + // src/components/album_menu_item.tsx + var import_react8 = __toESM(require_react()); + + // src/components/leading_icon.tsx var import_react5 = __toESM(require_react()); - var expandLibrary = () => { - Spicetify.Platform.LocalStorageAPI.setItem("ylx-sidebar-state", 0); - }; - var ExpandIcon = () => { - const { IconComponent } = Spicetify.ReactComponent; - return /* @__PURE__ */ import_react5.default.createElement(IconComponent, { + var LeadingIcon = ({ path }) => { + return /* @__PURE__ */ import_react5.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", dangerouslySetInnerHTML: { - __html: '' + __html: `${path}` }, iconSize: 16 }); }; - var ExpandButton = () => { - const { ButtonTertiary } = Spicetify.ReactComponent; - return /* @__PURE__ */ import_react5.default.createElement(ButtonTertiary, { - buttonSize: "sm", - "aria-label": "Show Filters", - iconOnly: ExpandIcon, - onClick: expandLibrary + var leading_icon_default = LeadingIcon; + + // src/components/text_input_dialog.tsx + var import_react6 = __toESM(require_react()); + var TextInputDialog = (props) => { + const { def, placeholder, onSave } = props; + const [value, setValue] = import_react6.default.useState(def || ""); + const onSubmit = (e) => { + e.preventDefault(); + Spicetify.PopupModal.hide(); + onSave(value); + }; + return /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, /* @__PURE__ */ import_react6.default.createElement("form", { + className: "text-input-form", + onSubmit + }, /* @__PURE__ */ import_react6.default.createElement("label", { + className: "text-input-wrapper" + }, /* @__PURE__ */ import_react6.default.createElement("input", { + className: "text-input", + type: "text", + value, + placeholder, + onChange: (e) => setValue(e.target.value) + })), /* @__PURE__ */ import_react6.default.createElement("button", { + type: "submit", + "data-encore-id": "buttonPrimary", + className: "Button-sc-qlcn5g-0 Button-small-buttonPrimary" + }, /* @__PURE__ */ import_react6.default.createElement("span", { + className: "ButtonInner-sc-14ud5tc-0 ButtonInner-small encore-bright-accent-set" + }, "Save")))); + }; + var text_input_dialog_default = TextInputDialog; + + // src/components/searchbar.tsx + var import_react7 = __toESM(require_react()); + var SearchBar = (props) => { + const { setSearch, placeholder } = props; + const handleChange = (e) => { + setSearch(e.target.value); + }; + return /* @__PURE__ */ import_react7.default.createElement("div", { + className: "x-filterBox-filterInputContainer x-filterBox-expandedOrHasFilter", + role: "search" + }, /* @__PURE__ */ import_react7.default.createElement("input", { + type: "text", + className: "x-filterBox-filterInput", + role: "searchbox", + maxLength: 80, + autoCorrect: "off", + autoCapitalize: "off", + spellCheck: "false", + placeholder: `Search ${placeholder}`, + "aria-hidden": "false", + onChange: handleChange + }), /* @__PURE__ */ import_react7.default.createElement("div", { + className: "x-filterBox-overlay" + }, /* @__PURE__ */ import_react7.default.createElement("span", { + className: "x-filterBox-searchIconContainer" + }, /* @__PURE__ */ import_react7.default.createElement("svg", { + "data-encore-id": "icon", + role: "img", + "aria-hidden": "true", + className: "Svg-sc-ytk21e-0 Svg-img-icon-small x-filterBox-searchIcon", + viewBox: "0 0 16 16" + }, /* @__PURE__ */ import_react7.default.createElement("path", { + d: "M7 1.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5zM.25 7a6.75 6.75 0 1 1 12.096 4.12l3.184 3.185a.75.75 0 1 1-1.06 1.06L11.304 12.2A6.75 6.75 0 0 1 .25 7z" + })))), /* @__PURE__ */ import_react7.default.createElement("button", { + className: "x-filterBox-expandButton", + "aria-hidden": "false", + "aria-label": "Search Playlists", + type: "button" + }, /* @__PURE__ */ import_react7.default.createElement("svg", { + "data-encore-id": "icon", + role: "img", + "aria-hidden": "true", + className: "Svg-sc-ytk21e-0 Svg-img-icon-small x-filterBox-searchIcon", + viewBox: "0 0 16 16" + }, /* @__PURE__ */ import_react7.default.createElement("path", { + d: "M7 1.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5zM.25 7a6.75 6.75 0 1 1 12.096 4.12l3.184 3.185a.75.75 0 1 1-1.06 1.06L11.304 12.2A6.75 6.75 0 0 1 .25 7z" + })))); + }; + var searchbar_default = SearchBar; + + // src/components/album_menu_item.tsx + var createCollection = () => { + const onSave = (value) => { + CollectionsWrapper.createCollection(value); + }; + Spicetify.PopupModal.display({ + title: "Create Collection", + content: /* @__PURE__ */ import_react8.default.createElement(text_input_dialog_default, { + def: "New Collection", + placeholder: "Collection Name", + onSave + }) }); }; - var expand_button_default = ExpandButton; + var CollectionSearchMenu = () => { + const { MenuItem } = Spicetify.ReactComponent; + const { SVGIcons } = Spicetify; + const [textFilter, setTextFilter] = import_react8.default.useState(""); + const [collections, setCollections] = import_react8.default.useState(null); + const context = import_react8.default.useContext(Spicetify.ContextMenuV2._context); + const uri = context?.props?.uri; + import_react8.default.useEffect(() => { + const fetchCollections = async () => { + setCollections(await CollectionsWrapper.getContents({ textFilter, limit: 20, offset: 0 })); + }; + fetchCollections(); + }, [textFilter]); + if (!collections) + return /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null); + const addToCollection = (collectionUri) => { + CollectionsWrapper.addAlbumToCollection(collectionUri, uri); + }; + const activeCollections = CollectionsWrapper.getCollectionsWithAlbum(uri); + const hasCollections = activeCollections.length > 0; + const removeFromCollections = () => { + for (const collection of activeCollections) { + CollectionsWrapper.removeAlbumFromCollection(collection.uri, uri); + } + }; + const allCollectionsLength = collections.totalLength; + const menuItems = collections.items.map((collection, index) => { + return /* @__PURE__ */ import_react8.default.createElement(MenuItem, { + key: collection.uri, + onClick: () => { + addToCollection(collection.uri); + }, + divider: index === 0 ? "before" : void 0 + }, collection.name); + }); + const menuLength = allCollectionsLength + (hasCollections ? 1 : 0); + return /* @__PURE__ */ import_react8.default.createElement("div", { + className: "main-contextMenu-filterPlaylistSearchContainer", + style: { "--context-menu-submenu-length": `${menuLength}` } + }, /* @__PURE__ */ import_react8.default.createElement("li", { + role: "presentation", + className: "main-contextMenu-filterPlaylistSearch" + }, /* @__PURE__ */ import_react8.default.createElement("div", { + role: "menuitem" + }, /* @__PURE__ */ import_react8.default.createElement(searchbar_default, { + setSearch: setTextFilter, + placeholder: "collections" + }))), /* @__PURE__ */ import_react8.default.createElement(MenuItem, { + key: "new-collection", + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: SVGIcons.plus2px + }), + onClick: createCollection + }, "Create collection"), hasCollections && /* @__PURE__ */ import_react8.default.createElement(MenuItem, { + key: "remove-collection", + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: SVGIcons.minus + }), + onClick: removeFromCollections + }, "Remove from all"), menuItems); + }; + var AlbumMenuItem = () => { + const { MenuSubMenuItem } = Spicetify.ReactComponent; + const { SVGIcons } = Spicetify; + return /* @__PURE__ */ import_react8.default.createElement(MenuSubMenuItem, { + displayText: "Add to collection", + divider: "after", + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: SVGIcons.plus2px + }) + }, /* @__PURE__ */ import_react8.default.createElement(CollectionSearchMenu, null)); + }; + var album_menu_item_default = AlbumMenuItem; + + // src/components/artist_menu_item.tsx + var import_react9 = __toESM(require_react()); + var ArtistMenuItem = () => { + const { MenuItem } = Spicetify.ReactComponent; + const { SVGIcons } = Spicetify; + const context = import_react9.default.useContext(Spicetify.ContextMenuV2._context); + const uri = context?.props?.uri; + return /* @__PURE__ */ import_react9.default.createElement(MenuItem, { + divider: "after", + leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { + path: SVGIcons.plus2px + }), + onClick: () => CollectionsWrapper.createCollectionFromDiscog(uri) + }, "Create Discog Collection"); + }; + var artist_menu_item_default = ArtistMenuItem; // ../node_modules/uuid/dist/esm-browser/rng.js var getRandomValues; @@ -407,7 +585,7 @@ var library = (() => { var v4_default = v4; // src/extensions/collections_wrapper.ts - var CollectionWrapper = class extends EventTarget { + var _CollectionsWrapper = class extends EventTarget { _collections; constructor() { super(); @@ -420,115 +598,189 @@ var library = (() => { getCollection(uri) { return this._collections.find((collection) => collection.uri === uri); } - async requestAlbums({ sortOrder, textFilter }) { + async getCollectionContents(uri) { + const collection = this.getCollection(uri); + if (!collection) + throw new Error("Collection not found"); + const items = this._collections.filter((collection2) => collection2.parentCollection === uri); const albums = await Spicetify.Platform.LibraryAPI.getContents({ filters: ["0"], - sortOrder, - textFilter, offset: 0, limit: 9999 }); - return albums; + items.push(...albums.items.filter((album) => collection.items.includes(album.uri))); + return items; } - async getCollectionItems(props) { - const { collectionUri, textFilter, sortOrder, rootlist, limit = 9999, offset = 0 } = props; - let collectionItems = this._collections; - let albumItems = []; - let unfilteredLength = this._collections.length; - let openedCollection = ""; - if (collectionUri) { - const collection = this.getCollection(collectionUri); - const res = await this.requestAlbums({ sortOrder, textFilter }); - const collectionSet = new Set(collection.items); - const commonElements = res.items.filter((item) => collectionSet.has(item.uri)); - const collections = this._collections.filter((collection2) => collection2.parentCollection === collectionUri); - openedCollection = collection.name; - collectionItems = collections; - albumItems = commonElements; - unfilteredLength = collection.totalLength; - } + async getContents(props) { + const { collectionUri, offset, limit, textFilter } = props; + let items = collectionUri ? await this.getCollectionContents(collectionUri) : this._collections; + const openedCollectionName = collectionUri ? this.getCollection(collectionUri)?.name : void 0; if (textFilter) { - let regex = new RegExp("\\b" + textFilter, "i"); - collectionItems = collectionItems.filter((item) => { - return regex.test(item.name); - }); + const regex = new RegExp(`\\b${textFilter}`, "i"); + items = items.filter((collection) => regex.test(collection.name)); } - if (rootlist && !collectionUri) { - const res = await this.requestAlbums({ sortOrder, textFilter }); - albumItems = res.items; - if (!textFilter) { - const collectionSet = new Set(this._collections.map((collection) => collection.items).flat()); - const uncommonElements = res.items.filter((item) => !collectionSet.has(item.uri)); - collectionItems = this._collections.filter((collection) => !collection.parentCollection); - albumItems = uncommonElements; - unfilteredLength = this._collections.length + uncommonElements.length; + items = items.slice(offset, offset + limit); + return { items, totalLength: this._collections.length, offset, openedCollectionName }; + } + async cleanCollections() { + for (const collection of this._collections) { + const boolArray = await Spicetify.Platform.LibraryAPI.contains(...collection.items); + if (boolArray.includes(false)) { + collection.items = collection.items.filter((_, i) => boolArray[i]); + this.saveCollections(); + Spicetify.showNotification("Album removed from collection"); + this.syncCollection(collection.uri); } } - if (offset > 0) - collectionItems = []; - return { - openedCollection, - items: [...collectionItems, ...albumItems.slice(offset, offset + limit)], - totalLength: albumItems.length + collectionItems.length, - unfilteredLength - }; + } + async syncCollection(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + const { PlaylistAPI } = Spicetify.Platform; + if (!collection.syncedPlaylistUri) + return; + const playlist = await PlaylistAPI.getPlaylist(collection.syncedPlaylistUri); + const playlistTracks = playlist.contents.items.filter((t) => t.type === "track").map((t) => t.uri); + const collectionTracks = await this.getTracklist(uri); + const wanted = collectionTracks.filter((track) => !playlistTracks.includes(track)); + const unwanted = playlistTracks.filter((track) => !collectionTracks.includes(track)).map((uri2) => ({ uri: uri2, uid: [] })); + if (wanted.length) + await PlaylistAPI.add(collection.syncedPlaylistUri, wanted, { before: "end" }); + if (unwanted.length) + await PlaylistAPI.remove(collection.syncedPlaylistUri, unwanted); + } + unsyncCollection(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + collection.syncedPlaylistUri = void 0; + this.saveCollections(); + } + async getTracklist(collectionUri) { + const collection = this.getCollection(collectionUri); + if (!collection) + return []; + return Promise.all( + collection.items.map(async (uri) => { + const album = await Spicetify.Platform.LibraryAPI.getAlbum(uri); + return album.items.map((t) => t.uri); + }) + ).then((tracks) => tracks.flat()); + } + async convertToPlaylist(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + const { Platform, showNotification } = Spicetify; + const { RootlistAPI, PlaylistAPI } = Platform; + if (collection.syncedPlaylistUri) { + showNotification("Synced Playlist already exists", true); + return; + } + try { + const playlistUri = await RootlistAPI.createPlaylist(collection.name, { before: "start" }); + const items = await this.getTracklist(uri); + await PlaylistAPI.add(playlistUri, items, { before: "start" }); + collection.syncedPlaylistUri = playlistUri; + } catch (error) { + console.error(error); + showNotification("Failed to create playlist", true); + } + } + async createCollectionFromDiscog(artistUri) { + const [raw, info] = await Promise.all([ + Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryArtistDiscographyAlbums, { + uri: artistUri, + offset: 0, + limit: 50 + }), + Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryArtistOverview, { + uri: artistUri, + locale: Spicetify.Locale.getLocale(), + includePrerelease: false + }) + ]); + const items = raw?.data?.artistUnion.discography.albums?.items; + const name = info?.data?.artistUnion.profile.name; + const image = info?.data?.artistUnion.visuals.avatarImage?.sources?.[0]?.url; + if (!name || !items?.length) { + Spicetify.showNotification("Artist not found or has no albums"); + return; + } + const collectionUri = this.createCollection(`${name} Albums`); + if (image) + this.setCollectionImage(collectionUri, image); + for (const album of items) { + this.addAlbumToCollection(collectionUri, album.releases.items[0].uri); + } } createCollection(name, parentCollection = "") { - const uri = v4_default(); - const collection = { + const id = v4_default(); + this._collections.push({ type: "collection", - uri, + uri: id, name, items: [], - totalLength: 0, - imgUrl: "", + addedAt: new Date(), + lastPlayedAt: new Date(), parentCollection - }; - this._collections.push(collection); + }); this.saveCollections(); Spicetify.showNotification("Collection created"); + return id; } deleteCollection(uri) { this._collections = this._collections.filter((collection) => collection.uri !== uri); this.saveCollections(); Spicetify.showNotification("Collection deleted"); } + deleteCollectionAndAlbums(uri) { + const collection = this.getCollection(uri); + if (!collection) + return; + for (const album of collection.items) { + Spicetify.Platform.LibraryAPI.remove({ uris: [album] }); + } + this.deleteCollection(uri); + } async addAlbumToCollection(collectionUri, albumUri) { const collection = this.getCollection(collectionUri); if (!collection) return; + await Spicetify.Platform.LibraryAPI.add({ uris: [albumUri] }); collection.items.push(albumUri); - collection.totalLength++; this.saveCollections(); Spicetify.showNotification("Album added to collection"); + this.syncCollection(collectionUri); } removeAlbumFromCollection(collectionUri, albumUri) { const collection = this.getCollection(collectionUri); if (!collection) return; collection.items = collection.items.filter((item) => item !== albumUri); - collection.totalLength--; this.saveCollections(); Spicetify.showNotification("Album removed from collection"); + this.syncCollection(collectionUri); } getCollectionsWithAlbum(albumUri) { return this._collections.filter((collection) => { return collection.items.some((item) => item === albumUri); }); } - renameCollection(uri, newName) { + renameCollection(uri, name) { const collection = this.getCollection(uri); if (!collection) return; - collection.name = newName; + collection.name = name; this.saveCollections(); Spicetify.showNotification("Collection renamed"); } - setCollectionImage(uri, imgUrl) { + setCollectionImage(uri, url) { const collection = this.getCollection(uri); if (!collection) return; - collection.imgUrl = imgUrl; + collection.image = url; this.saveCollections(); Spicetify.showNotification("Collection image set"); } @@ -536,201 +788,17 @@ var library = (() => { const collection = this.getCollection(uri); if (!collection) return; - collection.imgUrl = ""; + collection.image = void 0; this.saveCollections(); Spicetify.showNotification("Collection image removed"); } }; - var collections_wrapper_default = CollectionWrapper; - - // src/components/album_menu_item.tsx - var import_react9 = __toESM(require_react()); - - // src/components/leading_icon.tsx - var import_react6 = __toESM(require_react()); - var LeadingIcon = ({ path }) => { - return /* @__PURE__ */ import_react6.default.createElement(Spicetify.ReactComponent.IconComponent, { - semanticColor: "textSubdued", - dangerouslySetInnerHTML: { - __html: `${path}` - }, - iconSize: 16 - }); - }; - var leading_icon_default = LeadingIcon; - - // src/components/text_input_dialog.tsx - var import_react7 = __toESM(require_react()); - var TextInputDialog = (props) => { - const { ButtonPrimary } = Spicetify.ReactComponent; - const { def, placeholder, onSave } = props; - const [value, setValue] = import_react7.default.useState(def); - const onSubmit = (e) => { - e.preventDefault(); - Spicetify.PopupModal.hide(); - onSave(value); - }; - return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, /* @__PURE__ */ import_react7.default.createElement("form", { - className: "text-input-form", - onSubmit - }, /* @__PURE__ */ import_react7.default.createElement("label", { - className: "text-input-wrapper" - }, /* @__PURE__ */ import_react7.default.createElement("input", { - className: "text-input", - type: "text", - value, - placeholder, - onChange: (e) => setValue(e.target.value) - })), /* @__PURE__ */ import_react7.default.createElement("button", { - type: "submit", - "data-encore-id": "buttonPrimary", - className: "Button-sc-qlcn5g-0 Button-small-buttonPrimary" - }, /* @__PURE__ */ import_react7.default.createElement("span", { - className: "ButtonInner-sc-14ud5tc-0 ButtonInner-small encore-bright-accent-set" - }, "Save")))); - }; - var text_input_dialog_default = TextInputDialog; - - // src/components/searchbar.tsx - var import_react8 = __toESM(require_react()); - var SearchBar = (props) => { - const { setSearch, placeholder } = props; - const handleChange = (e) => { - setSearch(e.target.value); - }; - return /* @__PURE__ */ import_react8.default.createElement("div", { - className: "x-filterBox-filterInputContainer x-filterBox-expandedOrHasFilter", - role: "search" - }, /* @__PURE__ */ import_react8.default.createElement("input", { - type: "text", - className: "x-filterBox-filterInput", - role: "searchbox", - maxLength: 80, - autoCorrect: "off", - autoCapitalize: "off", - spellCheck: "false", - placeholder: `Search ${placeholder}`, - "aria-hidden": "false", - onChange: handleChange - }), /* @__PURE__ */ import_react8.default.createElement("div", { - className: "x-filterBox-overlay" - }, /* @__PURE__ */ import_react8.default.createElement("span", { - className: "x-filterBox-searchIconContainer" - }, /* @__PURE__ */ import_react8.default.createElement("svg", { - "data-encore-id": "icon", - role: "img", - "aria-hidden": "true", - className: "Svg-sc-ytk21e-0 Svg-img-icon-small x-filterBox-searchIcon", - viewBox: "0 0 16 16" - }, /* @__PURE__ */ import_react8.default.createElement("path", { - d: "M7 1.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5zM.25 7a6.75 6.75 0 1 1 12.096 4.12l3.184 3.185a.75.75 0 1 1-1.06 1.06L11.304 12.2A6.75 6.75 0 0 1 .25 7z" - })))), /* @__PURE__ */ import_react8.default.createElement("button", { - className: "x-filterBox-expandButton", - "aria-hidden": "false", - "aria-label": "Search Playlists" - }, /* @__PURE__ */ import_react8.default.createElement("svg", { - "data-encore-id": "icon", - role: "img", - "aria-hidden": "true", - className: "Svg-sc-ytk21e-0 Svg-img-icon-small x-filterBox-searchIcon", - viewBox: "0 0 16 16" - }, /* @__PURE__ */ import_react8.default.createElement("path", { - d: "M7 1.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5zM.25 7a6.75 6.75 0 1 1 12.096 4.12l3.184 3.185a.75.75 0 1 1-1.06 1.06L11.304 12.2A6.75 6.75 0 0 1 .25 7z" - })))); - }; - var searchbar_default = SearchBar; - - // src/components/album_menu_item.tsx - var createCollection = () => { - const onSave = (value) => { - SpicetifyLibrary.CollectionWrapper.createCollection(value); - }; - Spicetify.PopupModal.display({ - title: "Create Collection", - content: /* @__PURE__ */ import_react9.default.createElement(text_input_dialog_default, { - def: "New Collection", - placeholder: "Collection Name", - onSave - }) - }); - }; - var CollectionSearchMenu = () => { - const { MenuItem } = Spicetify.ReactComponent; - const { SVGIcons } = Spicetify; - const [textFilter, setTextFilter] = import_react9.default.useState(""); - const [collections, setCollections] = import_react9.default.useState(null); - const context = import_react9.default.useContext(Spicetify.ContextMenuV2._context); - const uri = context?.props?.uri; - import_react9.default.useEffect(() => { - const fetchCollections = async () => { - const res = await SpicetifyLibrary.CollectionWrapper.getCollectionItems({ textFilter }); - setCollections(res); - }; - fetchCollections(); - }, [textFilter]); - if (!collections) - return /* @__PURE__ */ import_react9.default.createElement(import_react9.default.Fragment, null); - const addToCollection = (collectionUri) => { - SpicetifyLibrary.CollectionWrapper.addAlbumToCollection(collectionUri, uri); - }; - const activeCollections = SpicetifyLibrary.CollectionWrapper.getCollectionsWithAlbum(uri); - const hasCollections = activeCollections.length > 0; - const removeFromCollections = () => { - activeCollections.forEach((collection) => { - SpicetifyLibrary.CollectionWrapper.removeAlbumFromCollection(collection.uri, uri); - }); - }; - const allCollectionsLength = collections.unfilteredLength; - const menuItems = collections.items.map((collection, index) => { - return /* @__PURE__ */ import_react9.default.createElement(MenuItem, { - key: collection.uri, - onClick: () => { - addToCollection(collection.uri); - }, - divider: index === 0 ? "before" : void 0 - }, collection.name); - }); - const menuLength = allCollectionsLength + (hasCollections ? 1 : 0); - return /* @__PURE__ */ import_react9.default.createElement("div", { - className: "main-contextMenu-filterPlaylistSearchContainer", - style: { "--context-menu-submenu-length": `${menuLength}` } - }, /* @__PURE__ */ import_react9.default.createElement("li", { - role: "presentation", - className: "main-contextMenu-filterPlaylistSearch" - }, /* @__PURE__ */ import_react9.default.createElement("div", { - role: "menuitem" - }, /* @__PURE__ */ import_react9.default.createElement(searchbar_default, { - setSearch: setTextFilter, - placeholder: "collections" - }))), /* @__PURE__ */ import_react9.default.createElement(MenuItem, { - key: "new-collection", - leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { - path: SVGIcons["plus2px"] - }), - onClick: createCollection - }, "Create collection"), hasCollections && /* @__PURE__ */ import_react9.default.createElement(MenuItem, { - key: "remove-collection", - leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { - path: SVGIcons["minus"] - }), - onClick: removeFromCollections - }, "Remove from all"), menuItems); - }; - var AlbumMenuItem = () => { - const { MenuSubMenuItem } = Spicetify.ReactComponent; - const { SVGIcons } = Spicetify; - return /* @__PURE__ */ import_react9.default.createElement(MenuSubMenuItem, { - displayText: "Add to collection", - divider: "after", - leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { - path: SVGIcons["plus2px"] - }) - }, /* @__PURE__ */ import_react9.default.createElement(CollectionSearchMenu, null)); - }; - var album_menu_item_default = AlbumMenuItem; + var CollectionsWrapper2 = _CollectionsWrapper; + __publicField(CollectionsWrapper2, "INSTANCE", new _CollectionsWrapper()); + window.CollectionsWrapper = CollectionsWrapper2.INSTANCE; // src/extensions/folder_image_wrapper.ts - var FolderImageWrapper = class extends EventTarget { + var _FolderImageWrapper = class extends EventTarget { _folderImages; constructor() { super(); @@ -757,7 +825,9 @@ var library = (() => { localStorage.setItem("library:folderImages", JSON.stringify(this._folderImages)); } }; - var folder_image_wrapper_default = FolderImageWrapper; + var FolderImageWrapper2 = _FolderImageWrapper; + __publicField(FolderImageWrapper2, "INSTANCE", new _FolderImageWrapper()); + window.FolderImageWrapper = FolderImageWrapper2.INSTANCE; // src/extensions/extension.tsx var styleLink = document.createElement("link"); @@ -773,6 +843,7 @@ var library = (() => { }; var FolderImage = ({ url }) => { return /* @__PURE__ */ import_react10.default.createElement("img", { + alt: "Folder Image", "aria-hidden": "true", draggable: "false", loading: "eager", @@ -793,7 +864,7 @@ var library = (() => { d: "M1 4a2 2 0 0 1 2-2h5.155a3 3 0 0 1 2.598 1.5l.866 1.5H21a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4zm7.155 0H3v16h18V7H10.464L9.021 4.5a1 1 0 0 0-.866-.5z" }))); }; - var SpicetifyLibrary2 = class { + var SpicetifyLibrary = class { ConfigWrapper = new config_wrapper_default( [ { @@ -812,16 +883,25 @@ var library = (() => { type: "toggle", def: false, callback: setSearchBarSize - } + }, + { + name: "Playlists Page", + key: "show-playlists", + type: "toggle", + def: true, + sectionHeader: "Pages" + }, + { name: "Albums Page", key: "show-albums", type: "toggle", def: true }, + { name: "Collections Page", key: "show-collections", type: "toggle", def: true }, + { name: "Artists Page", key: "show-artists", type: "toggle", def: true }, + { name: "Shows Page", key: "show-shows", type: "toggle", def: true } ], "library" ); - CollectionWrapper = new collections_wrapper_default(); - FolderImageWrapper = new folder_image_wrapper_default(); }; - window.SpicetifyLibrary = new SpicetifyLibrary2(); + window.SpicetifyLibrary = new SpicetifyLibrary(); (function wait() { - const { LocalStorageAPI } = Spicetify?.Platform; + const { LocalStorageAPI } = Spicetify.Platform; if (!LocalStorageAPI) { setTimeout(wait, 100); return; @@ -829,22 +909,23 @@ var library = (() => { main(LocalStorageAPI); })(); function main(LocalStorageAPI) { - const isAlbum = (props) => { - return props.uri?.includes("album"); - }; + const isAlbum = (props) => props.uri?.includes("album"); + const isArtist = (props) => props.uri?.includes("artist"); Spicetify.ContextMenuV2.registerItem(/* @__PURE__ */ import_react10.default.createElement(album_menu_item_default, null), isAlbum); + Spicetify.ContextMenuV2.registerItem(/* @__PURE__ */ import_react10.default.createElement(artist_menu_item_default, null), isArtist); + Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", () => CollectionsWrapper.cleanCollections()); function injectFolderImages() { const rootlist = document.querySelector(".main-rootlist-wrapper > div:nth-child(2)"); if (!rootlist) return setTimeout(injectFolderImages, 100); setTimeout(() => { - Array.from(rootlist.children).forEach((el) => { - const uri = el.querySelector(".main-yourLibraryX-listItemGroup")?.getAttribute("aria-labelledby")?.slice(14); + for (const el of Array.from(rootlist.children)) { + const uri = el.querySelector("[aria-labelledby]")?.getAttribute("aria-labelledby")?.slice(14); if (uri?.includes("folder")) { const imageBox = el.querySelector(".x-entityImage-imageContainer"); if (!imageBox) return; - const imageUrl = window.SpicetifyLibrary.FolderImageWrapper.getFolderImage(uri); + const imageUrl = FolderImageWrapper.getFolderImage(uri); if (!imageUrl) import_react_dom.default.render(/* @__PURE__ */ import_react10.default.createElement(FolderPlaceholder, null), imageBox); else @@ -852,17 +933,13 @@ var library = (() => { url: imageUrl }), imageBox); } - }); + } }, 500); } injectFolderImages(); - window.SpicetifyLibrary.FolderImageWrapper.addEventListener("update", () => { - injectFolderImages(); - }); + FolderImageWrapper.addEventListener("update", injectFolderImages); function injectYLXButtons() { - const ylx_filter = document.querySelector( - ".main-yourLibraryX-libraryRootlist > .main-yourLibraryX-libraryFilter" - ); + const ylx_filter = document.querySelector(".main-yourLibraryX-libraryRootlist > .main-yourLibraryX-libraryFilter"); if (!ylx_filter) { return setTimeout(injectYLXButtons, 100); } @@ -887,37 +964,17 @@ var library = (() => { collapseButton ); } - function injectExpandButton() { - const sidebarHeader = document.querySelector("li.main-yourLibraryX-navItem[data-id='/library']"); - if (!sidebarHeader) { - return setTimeout(injectExpandButton, 100); - } - const expandButton = document.createElement("span"); - expandButton.classList.add("expand-button"); - sidebarHeader.appendChild(expandButton); - import_react_dom.default.render(/* @__PURE__ */ import_react10.default.createElement(expand_button_default, null), expandButton); - } - function removeExpandButton() { - const expandButton = document.querySelector(".expand-button"); - if (expandButton) - expandButton.remove(); - } const state = LocalStorageAPI.getItem("ylx-sidebar-state"); - if (state === 0) { + if (state === 0) injectYLXButtons(); - } else if (state === 1) { - injectExpandButton(); - } LocalStorageAPI.getEvents()._emitter.addListener("update", (e) => { const { key, value } = e.data; if (key === "ylx-sidebar-state" && value === 0) { injectFolderImages(); injectYLXButtons(); - removeExpandButton(); } if (key === "ylx-sidebar-state" && value === 1) { injectFolderImages(); - injectExpandButton(); } }); } diff --git a/.config/spicetify/CustomApps/library/folder_image_wrapper.js b/.config/spicetify/CustomApps/library/folder_image_wrapper.js index 57f0f545..b8711081 100644 --- a/.config/spicetify/CustomApps/library/folder_image_wrapper.js +++ b/.config/spicetify/CustomApps/library/folder_image_wrapper.js @@ -8,6 +8,7 @@ var library = (() => { var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); @@ -21,13 +22,17 @@ var library = (() => { return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; + }; // src/extensions/folder_image_wrapper.ts var folder_image_wrapper_exports = {}; __export(folder_image_wrapper_exports, { default: () => folder_image_wrapper_default }); - var FolderImageWrapper = class extends EventTarget { + var _FolderImageWrapper = class extends EventTarget { _folderImages; constructor() { super(); @@ -54,6 +59,9 @@ var library = (() => { localStorage.setItem("library:folderImages", JSON.stringify(this._folderImages)); } }; + var FolderImageWrapper = _FolderImageWrapper; + __publicField(FolderImageWrapper, "INSTANCE", new _FolderImageWrapper()); + window.FolderImageWrapper = FolderImageWrapper.INSTANCE; var folder_image_wrapper_default = FolderImageWrapper; return __toCommonJS(folder_image_wrapper_exports); })(); diff --git a/.config/spicetify/CustomApps/library/index.js b/.config/spicetify/CustomApps/library/index.js index 326195fd..2e4b7eb6 100644 --- a/.config/spicetify/CustomApps/library/index.js +++ b/.config/spicetify/CustomApps/library/index.js @@ -47,7 +47,7 @@ var library = (() => { }); // src/app.tsx - var import_react23 = __toESM(require_react()); + var import_react26 = __toESM(require_react()); // ../node_modules/spcr-navigation-bar/useNavigationBar.tsx var import_react3 = __toESM(require_react()); @@ -105,7 +105,7 @@ var library = (() => { }); var optionsMenu_default = OptionsMenu; - // postcss-module:C:\Users\user\AppData\Local\Temp\tmp-5152-1Ex7Bvh4Tu6u\18f3cb8bc8b4\navBar.module.css + // postcss-module:C:\Users\user\AppData\Local\Temp\tmp-4464-wbZ6O1BKhuot\19178f16f304\navBar.module.css var navBar_module_default = { "topBarHeaderItem": "navBar-module__topBarHeaderItem___piw4C_library", "topBarHeaderItemLink": "navBar-module__topBarHeaderItemLink___xA4uv_library", "topBarActive": "navBar-module__topBarActive___XhWpm_library", "topBarNav": "navBar-module__topBarNav___qWGeZ_library", "optionsMenuDropBox": "navBar-module__optionsMenuDropBox___pzfNI_library" }; // ../node_modules/spcr-navigation-bar/navBar.tsx @@ -233,7 +233,7 @@ var library = (() => { var useNavigationBar_default = useNavigationBar; // src/pages/albums.tsx - var import_react19 = __toESM(require_react()); + var import_react21 = __toESM(require_react()); // src/components/searchbar.tsx var import_react4 = __toESM(require_react()); @@ -271,7 +271,8 @@ var library = (() => { })))), /* @__PURE__ */ import_react4.default.createElement("button", { className: "x-filterBox-expandButton", "aria-hidden": "false", - "aria-label": "Search Playlists" + "aria-label": "Search Playlists", + type: "button" }, /* @__PURE__ */ import_react4.default.createElement("svg", { "data-encore-id": "icon", role: "img", @@ -458,87 +459,44 @@ var library = (() => { }; var page_container_default = PageContainer; - // ../shared/components/status.tsx - var import_react10 = __toESM(require_react()); - var ErrorIcon = () => { - return /* @__PURE__ */ import_react10.default.createElement("svg", { - "data-encore-id": "icon", - role: "img", - "aria-hidden": "true", - viewBox: "0 0 24 24", - className: "status-icon" - }, /* @__PURE__ */ import_react10.default.createElement("path", { - d: "M11 18v-2h2v2h-2zm0-4V6h2v8h-2z" - }), /* @__PURE__ */ import_react10.default.createElement("path", { - d: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM1 12C1 5.925 5.925 1 12 1s11 4.925 11 11-4.925 11-11 11S1 18.075 1 12z" - })); - }; - var LibraryIcon = () => { - return /* @__PURE__ */ import_react10.default.createElement("svg", { - role: "img", - height: "46", - width: "46", - "aria-hidden": "true", - viewBox: "0 0 24 24", - "data-encore-id": "icon", - className: "status-icon" - }, /* @__PURE__ */ import_react10.default.createElement("path", { - d: "M14.5 2.134a1 1 0 0 1 1 0l6 3.464a1 1 0 0 1 .5.866V21a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1V3a1 1 0 0 1 .5-.866zM16 4.732V20h4V7.041l-4-2.309zM3 22a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1zm6 0a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1z" - })); - }; - var Status = (props) => { - const [isVisible, setIsVisible] = import_react10.default.useState(false); - import_react10.default.useEffect(() => { - const to = setTimeout(() => { - setIsVisible(true); - }, 500); - return () => clearTimeout(to); - }, []); - return isVisible ? /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null, /* @__PURE__ */ import_react10.default.createElement("div", { - className: "loadingWrapper" - }, props.icon === "error" ? /* @__PURE__ */ import_react10.default.createElement(ErrorIcon, null) : /* @__PURE__ */ import_react10.default.createElement(LibraryIcon, null), /* @__PURE__ */ import_react10.default.createElement("h1", null, props.heading), /* @__PURE__ */ import_react10.default.createElement("h3", null, props.subheading))) : /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null); - }; - var status_default = Status; - // src/components/collection_menu.tsx - var import_react13 = __toESM(require_react()); + var import_react12 = __toESM(require_react()); // src/components/text_input_dialog.tsx - var import_react11 = __toESM(require_react()); + var import_react10 = __toESM(require_react()); var TextInputDialog = (props) => { - const { ButtonPrimary } = Spicetify.ReactComponent; const { def, placeholder, onSave } = props; - const [value, setValue] = import_react11.default.useState(def); + const [value, setValue] = import_react10.default.useState(def || ""); const onSubmit = (e) => { e.preventDefault(); Spicetify.PopupModal.hide(); onSave(value); }; - return /* @__PURE__ */ import_react11.default.createElement(import_react11.default.Fragment, null, /* @__PURE__ */ import_react11.default.createElement("form", { + return /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null, /* @__PURE__ */ import_react10.default.createElement("form", { className: "text-input-form", onSubmit - }, /* @__PURE__ */ import_react11.default.createElement("label", { + }, /* @__PURE__ */ import_react10.default.createElement("label", { className: "text-input-wrapper" - }, /* @__PURE__ */ import_react11.default.createElement("input", { + }, /* @__PURE__ */ import_react10.default.createElement("input", { className: "text-input", type: "text", value, placeholder, onChange: (e) => setValue(e.target.value) - })), /* @__PURE__ */ import_react11.default.createElement("button", { + })), /* @__PURE__ */ import_react10.default.createElement("button", { type: "submit", "data-encore-id": "buttonPrimary", className: "Button-sc-qlcn5g-0 Button-small-buttonPrimary" - }, /* @__PURE__ */ import_react11.default.createElement("span", { + }, /* @__PURE__ */ import_react10.default.createElement("span", { className: "ButtonInner-sc-14ud5tc-0 ButtonInner-small encore-bright-accent-set" }, "Save")))); }; var text_input_dialog_default = TextInputDialog; // src/components/leading_icon.tsx - var import_react12 = __toESM(require_react()); + var import_react11 = __toESM(require_react()); var LeadingIcon = ({ path }) => { - return /* @__PURE__ */ import_react12.default.createElement(Spicetify.ReactComponent.IconComponent, { + return /* @__PURE__ */ import_react11.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", dangerouslySetInnerHTML: { __html: `${path}` @@ -551,32 +509,39 @@ var library = (() => { // src/components/collection_menu.tsx var editIconPath = ''; var deleteIconPath = ''; + var addIconPath = ''; var CollectionMenu = ({ id }) => { const { Menu, MenuItem: MenuItem2 } = Spicetify.ReactComponent; const deleteCollection = () => { - SpicetifyLibrary.CollectionWrapper.deleteCollection(id); + CollectionsWrapper.deleteCollection(id); + }; + const deleteCollectionAndAlbums = () => { + CollectionsWrapper.deleteCollectionAndAlbums(id); }; const renameCollection = () => { - const name = SpicetifyLibrary.CollectionWrapper.getCollection(id).name; + const name = CollectionsWrapper.getCollection(id)?.name; const rename = (newName) => { - SpicetifyLibrary.CollectionWrapper.renameCollection(id, newName); + CollectionsWrapper.renameCollection(id, newName); }; Spicetify.PopupModal.display({ title: "Rename Collection", - content: /* @__PURE__ */ import_react13.default.createElement(text_input_dialog_default, { + content: /* @__PURE__ */ import_react12.default.createElement(text_input_dialog_default, { def: name, + placeholder: "Collection Name", onSave: rename }) }); }; - const image = SpicetifyLibrary.CollectionWrapper.getCollection(id).imgUrl; + const collection = CollectionsWrapper.getCollection(id); + const image = collection?.image; + const synced = collection?.syncedPlaylistUri; const setCollectionImage = () => { const setImg = (imgUrl) => { - SpicetifyLibrary.CollectionWrapper.setCollectionImage(id, imgUrl); + CollectionsWrapper.setCollectionImage(id, imgUrl); }; Spicetify.PopupModal.display({ title: "Set Collection Image", - content: /* @__PURE__ */ import_react13.default.createElement(text_input_dialog_default, { + content: /* @__PURE__ */ import_react12.default.createElement(text_input_dialog_default, { def: image, placeholder: "Image URL", onSave: setImg @@ -584,25 +549,46 @@ var library = (() => { }); }; const removeImage = () => { - SpicetifyLibrary.CollectionWrapper.removeCollectionImage(id); + CollectionsWrapper.removeCollectionImage(id); }; - return /* @__PURE__ */ import_react13.default.createElement(Menu, null, /* @__PURE__ */ import_react13.default.createElement(MenuItem2, { - leadingIcon: /* @__PURE__ */ import_react13.default.createElement(leading_icon_default, { + const convertToPlaylist = () => { + CollectionsWrapper.convertToPlaylist(id); + }; + const unsyncPlaylist = () => { + CollectionsWrapper.unsyncCollection(id); + }; + return /* @__PURE__ */ import_react12.default.createElement(Menu, null, /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { path: editIconPath }), onClick: renameCollection - }, "Rename"), /* @__PURE__ */ import_react13.default.createElement(MenuItem2, { - leadingIcon: /* @__PURE__ */ import_react13.default.createElement(leading_icon_default, { + }, "Rename"), /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { path: deleteIconPath }), onClick: deleteCollection - }, "Delete"), /* @__PURE__ */ import_react13.default.createElement(MenuItem2, { - leadingIcon: /* @__PURE__ */ import_react13.default.createElement(leading_icon_default, { + }, "Delete (Only Collection)"), /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { + path: deleteIconPath + }), + onClick: deleteCollectionAndAlbums + }, "Delete (Collection and Albums)"), synced ? /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { + path: deleteIconPath + }), + onClick: unsyncPlaylist + }, "Unsync from Playlist") : /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { + path: addIconPath + }), + onClick: convertToPlaylist + }, "Sync to Playlist"), /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { path: editIconPath }), onClick: setCollectionImage - }, "Set Collection Image"), image && /* @__PURE__ */ import_react13.default.createElement(MenuItem2, { - leadingIcon: /* @__PURE__ */ import_react13.default.createElement(leading_icon_default, { + }, "Set Collection Image"), image && /* @__PURE__ */ import_react12.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react12.default.createElement(leading_icon_default, { path: deleteIconPath }), onClick: removeImage @@ -611,34 +597,35 @@ var library = (() => { var collection_menu_default = CollectionMenu; // src/components/folder_menu.tsx - var import_react14 = __toESM(require_react()); + var import_react13 = __toESM(require_react()); var editIconPath2 = ''; var deleteIconPath2 = ''; var FolderMenu = ({ uri }) => { const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; - const image = SpicetifyLibrary.FolderImageWrapper.getFolderImage(uri); + const image = FolderImageWrapper.getFolderImage(uri); const setImage = () => { const setNewImage = (newUrl) => { - SpicetifyLibrary.FolderImageWrapper.setFolderImage({ uri, url: newUrl }); + FolderImageWrapper.setFolderImage({ uri, url: newUrl }); }; Spicetify.PopupModal.display({ title: "Set Folder Image", - content: /* @__PURE__ */ import_react14.default.createElement(text_input_dialog_default, { + content: /* @__PURE__ */ import_react13.default.createElement(text_input_dialog_default, { def: image, - onSave: setNewImage + onSave: setNewImage, + placeholder: "Image URL" }) }); }; const removeImage = () => { - SpicetifyLibrary.FolderImageWrapper.removeFolderImage(uri); + FolderImageWrapper.removeFolderImage(uri); }; - return /* @__PURE__ */ import_react14.default.createElement(Menu, null, /* @__PURE__ */ import_react14.default.createElement(MenuItem2, { - leadingIcon: /* @__PURE__ */ import_react14.default.createElement(leading_icon_default, { + return /* @__PURE__ */ import_react13.default.createElement(Menu, null, /* @__PURE__ */ import_react13.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react13.default.createElement(leading_icon_default, { path: editIconPath2 }), onClick: setImage - }, "Set Folder Image"), image && /* @__PURE__ */ import_react14.default.createElement(MenuItem2, { - leadingIcon: /* @__PURE__ */ import_react14.default.createElement(leading_icon_default, { + }, "Set Folder Image"), image && /* @__PURE__ */ import_react13.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react13.default.createElement(leading_icon_default, { path: deleteIconPath2 }), onClick: removeImage @@ -647,12 +634,12 @@ var library = (() => { var folder_menu_default = FolderMenu; // ../shared/components/spotify_card.tsx - var import_react16 = __toESM(require_react()); + var import_react15 = __toESM(require_react()); // ../shared/components/folder_fallback.tsx - var import_react15 = __toESM(require_react()); + var import_react14 = __toESM(require_react()); var FolderSVG = (e) => { - return /* @__PURE__ */ import_react15.default.createElement(Spicetify.ReactComponent.IconComponent, { + return /* @__PURE__ */ import_react14.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", viewBox: "0 0 24 24", size: "xxlarge", @@ -669,41 +656,44 @@ var library = (() => { const { Cards, TextComponent, ArtistMenu, AlbumMenu, PodcastShowMenu, PlaylistMenu, ContextMenu } = Spicetify.ReactComponent; const { FeatureCard: Card, CardImage } = Cards; const { createHref, push } = Spicetify.Platform.History; - const { type, header, uri, imageUrl, subheader, artistUri } = props; - const backupImageUrl = type === "folder" || type === "collection" ? "https://raw.githubusercontent.com/harbassan/spicetify-apps/main/shared/placeholders/folder_placeholder.png" : "https://raw.githubusercontent.com/harbassan/spicetify-apps/main/shared/placeholders/def_placeholder.png"; + const { type, header, uri, imageUrl, subheader, artistUri, badge, provider } = props; const Menu = () => { switch (type) { case "artist": - return /* @__PURE__ */ import_react16.default.createElement(ArtistMenu, { + return /* @__PURE__ */ import_react15.default.createElement(ArtistMenu, { uri }); case "album": - return /* @__PURE__ */ import_react16.default.createElement(AlbumMenu, { + return /* @__PURE__ */ import_react15.default.createElement(AlbumMenu, { uri, artistUri, canRemove: true }); case "playlist": - return /* @__PURE__ */ import_react16.default.createElement(PlaylistMenu, { + return /* @__PURE__ */ import_react15.default.createElement(PlaylistMenu, { uri }); case "show": - return /* @__PURE__ */ import_react16.default.createElement(PodcastShowMenu, { + return /* @__PURE__ */ import_react15.default.createElement(PodcastShowMenu, { uri }); case "collection": - return /* @__PURE__ */ import_react16.default.createElement(collection_menu_default, { + return /* @__PURE__ */ import_react15.default.createElement(collection_menu_default, { id: uri }); case "folder": - return /* @__PURE__ */ import_react16.default.createElement(folder_menu_default, { + return /* @__PURE__ */ import_react15.default.createElement(folder_menu_default, { uri }); default: - return /* @__PURE__ */ import_react16.default.createElement(import_react16.default.Fragment, null); + return /* @__PURE__ */ import_react15.default.createElement(import_react15.default.Fragment, null); } }; - const lastfmProps = type === "lastfm" ? { onClick: () => window.open(uri, "_blank"), isPlayable: false, delegateNavigation: true } : {}; + const lastfmProps = provider === "lastfm" ? { + onClick: () => window.open(uri, "_blank"), + isPlayable: false, + delegateNavigation: true + } : {}; const folderProps = type === "folder" ? { delegateNavigation: true, onClick: () => { @@ -718,13 +708,15 @@ var library = (() => { push({ pathname: `/library/collection/${uri}` }); } } : {}; - return /* @__PURE__ */ import_react16.default.createElement(ContextMenu, { + return /* @__PURE__ */ import_react15.default.createElement(ContextMenu, { menu: Menu(), trigger: "right-click" - }, /* @__PURE__ */ import_react16.default.createElement(Card, { + }, /* @__PURE__ */ import_react15.default.createElement("div", { + style: { position: "relative" } + }, /* @__PURE__ */ import_react15.default.createElement(Card, { featureIdentifier: type, headerText: header, - renderCardImage: () => /* @__PURE__ */ import_react16.default.createElement(CardImage, { + renderCardImage: () => /* @__PURE__ */ import_react15.default.createElement(CardImage, { images: [ { height: 640, @@ -733,9 +725,9 @@ var library = (() => { } ], isCircular: type === "artist", - FallbackComponent: folder_fallback_default + FallbackComponent: type === "folder" || type === "collection" ? folder_fallback_default : void 0 }), - renderSubHeaderContent: () => /* @__PURE__ */ import_react16.default.createElement(TextComponent, { + renderSubHeaderContent: () => /* @__PURE__ */ import_react15.default.createElement(TextComponent, { as: "div", variant: "mesto", semanticColor: "textSubdued", @@ -745,48 +737,49 @@ var library = (() => { ...lastfmProps, ...folderProps, ...collectionProps - })); + }), badge && /* @__PURE__ */ import_react15.default.createElement("div", { + className: "badge" + }, badge))); } var spotify_card_default = SpotifyCard; // src/components/load_more_card.tsx - var import_react17 = __toESM(require_react()); + var import_react16 = __toESM(require_react()); var LoadMoreCard = (props) => { const { callback } = props; - return /* @__PURE__ */ import_react17.default.createElement("div", { + return /* @__PURE__ */ import_react16.default.createElement("div", { onClick: callback, className: "load-more-card main-card-card" - }, /* @__PURE__ */ import_react17.default.createElement("div", { + }, /* @__PURE__ */ import_react16.default.createElement("div", { className: "svg-placeholder" - }, /* @__PURE__ */ import_react17.default.createElement("svg", { + }, /* @__PURE__ */ import_react16.default.createElement("svg", { viewBox: "0 8 24 8", xmlns: "http://www.w3.org/2000/svg" - }, /* @__PURE__ */ import_react17.default.createElement("circle", { + }, /* @__PURE__ */ import_react16.default.createElement("circle", { cx: "17.5", cy: "12", r: "1.5" - }), /* @__PURE__ */ import_react17.default.createElement("circle", { + }), /* @__PURE__ */ import_react16.default.createElement("circle", { cx: "12", cy: "12", r: "1.5" - }), /* @__PURE__ */ import_react17.default.createElement("circle", { + }), /* @__PURE__ */ import_react16.default.createElement("circle", { cx: "6.5", cy: "12", r: "1.5" - }))), /* @__PURE__ */ import_react17.default.createElement(Spicetify.ReactComponent.TextComponent, { + }))), /* @__PURE__ */ import_react16.default.createElement(Spicetify.ReactComponent.TextComponent, { as: "div", variant: "violaBold", semanticColor: "textBase", - weight: "bold", - children: "Load More" - })); + weight: "bold" + }, "Load More")); }; var load_more_card_default = LoadMoreCard; // src/components/add_button.tsx - var import_react18 = __toESM(require_react()); + var import_react17 = __toESM(require_react()); function AddIcon() { - return /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.IconComponent, { + return /* @__PURE__ */ import_react17.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", dangerouslySetInnerHTML: { __html: '' @@ -798,13 +791,13 @@ var library = (() => { const { ReactComponent } = Spicetify; const { TooltipWrapper, ButtonTertiary, ContextMenu } = ReactComponent; const { Menu } = props; - return /* @__PURE__ */ import_react18.default.createElement(TooltipWrapper, { + return /* @__PURE__ */ import_react17.default.createElement(TooltipWrapper, { label: "Add", placement: "top" - }, /* @__PURE__ */ import_react18.default.createElement("span", null, /* @__PURE__ */ import_react18.default.createElement(ContextMenu, { + }, /* @__PURE__ */ import_react17.default.createElement("span", null, /* @__PURE__ */ import_react17.default.createElement(ContextMenu, { trigger: "click", menu: Menu - }, /* @__PURE__ */ import_react18.default.createElement(ButtonTertiary, { + }, /* @__PURE__ */ import_react17.default.createElement(ButtonTertiary, { buttonSize: "sm", "aria-label": "Add", iconOnly: AddIcon @@ -812,165 +805,204 @@ var library = (() => { } var add_button_default = AddButton; + // ../shared/status/useStatus.tsx + var import_react19 = __toESM(require_react()); + + // ../shared/status/status.tsx + var import_react18 = __toESM(require_react()); + var ErrorIcon = () => { + return /* @__PURE__ */ import_react18.default.createElement("svg", { + "data-encore-id": "icon", + role: "img", + "aria-hidden": "true", + viewBox: "0 0 24 24", + className: "status-icon" + }, /* @__PURE__ */ import_react18.default.createElement("path", { + d: "M11 18v-2h2v2h-2zm0-4V6h2v8h-2z" + }), /* @__PURE__ */ import_react18.default.createElement("path", { + d: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM1 12C1 5.925 5.925 1 12 1s11 4.925 11 11-4.925 11-11 11S1 18.075 1 12z" + })); + }; + var LibraryIcon = () => { + return /* @__PURE__ */ import_react18.default.createElement("svg", { + role: "img", + height: "46", + width: "46", + "aria-hidden": "true", + viewBox: "0 0 24 24", + "data-encore-id": "icon", + className: "status-icon" + }, /* @__PURE__ */ import_react18.default.createElement("path", { + d: "M14.5 2.134a1 1 0 0 1 1 0l6 3.464a1 1 0 0 1 .5.866V21a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1V3a1 1 0 0 1 .5-.866zM16 4.732V20h4V7.041l-4-2.309zM3 22a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1zm6 0a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1z" + })); + }; + var Status = (props) => { + const [isVisible, setIsVisible] = import_react18.default.useState(false); + import_react18.default.useEffect(() => { + const to = setTimeout(() => { + setIsVisible(true); + }, 500); + return () => clearTimeout(to); + }, []); + return isVisible ? /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement("div", { + className: "loadingWrapper" + }, props.icon === "error" ? /* @__PURE__ */ import_react18.default.createElement(ErrorIcon, null) : /* @__PURE__ */ import_react18.default.createElement(LibraryIcon, null), /* @__PURE__ */ import_react18.default.createElement("h1", null, props.heading), /* @__PURE__ */ import_react18.default.createElement("h3", null, props.subheading))) : /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null); + }; + var status_default = Status; + + // ../shared/status/useStatus.tsx + var useStatus = (status, error) => { + if (status === "pending") { + return /* @__PURE__ */ import_react19.default.createElement(status_default, { + icon: "library", + heading: "Loading", + subheading: "Please wait, this may take a moment" + }); + } + if (status === "error") { + return /* @__PURE__ */ import_react19.default.createElement(status_default, { + icon: "error", + heading: "Error", + subheading: error?.message || "An unknown error occurred" + }); + } + return null; + }; + var useStatus_default = useStatus; + + // ../shared/types/react_query.ts + var ReactQuery = Spicetify.ReactQuery; + var useQuery = ReactQuery.useQuery; + var QueryClient = ReactQuery.QueryClient; + var QueryClientProvider = ReactQuery.QueryClientProvider; + var useInfiniteQuery = ReactQuery.useInfiniteQuery; + + // src/components/pin_icon.tsx + var import_react20 = __toESM(require_react()); + var PinIcon = () => /* @__PURE__ */ import_react20.default.createElement(Spicetify.ReactComponent.IconComponent, { + semanticColor: "textBase", + viewBox: "0 0 16 16", + iconSize: 12 + }, /* @__PURE__ */ import_react20.default.createElement("path", { + d: "M8.822.797a2.72 2.72 0 0 1 3.847 0l2.534 2.533a2.72 2.72 0 0 1 0 3.848l-3.678 3.678-1.337 4.988-4.486-4.486L1.28 15.78a.75.75 0 0 1-1.06-1.06l4.422-4.422L.156 5.812l4.987-1.337L8.822.797z" + })); + var pin_icon_default = PinIcon; + // src/pages/albums.tsx - var sortOptions = [ - { id: "0", name: "Name" }, - { id: "1", name: "Date Added" }, - { id: "2", name: "Artist Name" }, - { id: "6", name: "Recents" } - ]; - var AddMenu = ({ collection }) => { + var AddMenu = () => { const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; const { SVGIcons } = Spicetify; - const createCollection = () => { - const onSave = (value) => { - SpicetifyLibrary.CollectionWrapper.createCollection(value, collection); - }; - Spicetify.PopupModal.display({ - title: "Create Collection", - content: /* @__PURE__ */ import_react19.default.createElement(text_input_dialog_default, { - def: "New Collection", - placeholder: "Collection Name", - onSave - }) - }); - }; const addAlbum = () => { const onSave = (value) => { - if (collection) - SpicetifyLibrary.CollectionWrapper.addAlbumToCollection(collection, value); Spicetify.Platform.LibraryAPI.add({ uris: [value] }); }; Spicetify.PopupModal.display({ title: "Add Album", - content: /* @__PURE__ */ import_react19.default.createElement(text_input_dialog_default, { + content: /* @__PURE__ */ import_react21.default.createElement(text_input_dialog_default, { def: "", placeholder: "Album URI", onSave }) }); }; - return /* @__PURE__ */ import_react19.default.createElement(Menu, null, /* @__PURE__ */ import_react19.default.createElement(MenuItem2, { - onClick: createCollection, - leadingIcon: /* @__PURE__ */ import_react19.default.createElement(leading_icon_default, { - path: SVGIcons["playlist-folder"] - }) - }, "Create Collection"), /* @__PURE__ */ import_react19.default.createElement(MenuItem2, { + return /* @__PURE__ */ import_react21.default.createElement(Menu, null, /* @__PURE__ */ import_react21.default.createElement(MenuItem2, { onClick: addAlbum, - leadingIcon: /* @__PURE__ */ import_react19.default.createElement(leading_icon_default, { - path: SVGIcons["album"] + leadingIcon: /* @__PURE__ */ import_react21.default.createElement(leading_icon_default, { + path: SVGIcons.album }) }, "Add Album")); }; - var AlbumsPage = ({ configWrapper, collection }) => { + var limit = 200; + var sortOptions = [ + { id: "0", name: "Name" }, + { id: "1", name: "Date Added" }, + { id: "2", name: "Artist Name" }, + { id: "6", name: "Recents" } + ]; + var AlbumsPage = ({ configWrapper }) => { const [dropdown, sortOption] = useDropdownMenu_default(sortOptions, "library:albums"); - const [textFilter, setTextFilter] = import_react19.default.useState(""); - const { useInfiniteQuery } = Spicetify.ReactQuery; - const limit = 200; - const fetchRootlist = async ({ pageParam }) => { - const collections = await SpicetifyLibrary.CollectionWrapper.getCollectionItems({ - collectionUri: collection, - textFilter, + const [textFilter, setTextFilter] = import_react21.default.useState(""); + const fetchAlbums = async ({ pageParam }) => { + const res = await Spicetify.Platform.LibraryAPI.getContents({ + filters: ["0"], sortOrder: sortOption.id, - limit, + textFilter, offset: pageParam, - rootlist: true + limit }); - return collections; + if (!res.items?.length) + throw new Error("No albums found"); + return res; }; - const { data, status, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ - queryKey: ["library:albums", sortOption.id, textFilter, collection], - queryFn: fetchRootlist, + const { data, status, error, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ + queryKey: ["library:albums", sortOption.id, textFilter], + queryFn: fetchAlbums, initialPageParam: 0, - getNextPageParam: (lastPage, _allPages, lastPageParam) => { - return lastPage.totalLength > lastPageParam + limit ? lastPageParam + limit : void 0; - }, - structuralSharing: false + getNextPageParam: (lastPage) => { + const current = lastPage.offset + limit; + if (lastPage.totalLength > current) + return current; + } }); - import_react19.default.useEffect(() => { - const onUpdate = (e) => { - refetch(); + (0, import_react21.useEffect)(() => { + const update = (e) => { + if (e.data.list === "albums") + refetch(); }; - Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", onUpdate); - SpicetifyLibrary.CollectionWrapper.addEventListener("update", onUpdate); + Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", update, {}); return () => { - Spicetify.Platform.LibraryAPI.getEvents()._emitter.removeListener("update", onUpdate); - SpicetifyLibrary.CollectionWrapper.removeEventListener("update", onUpdate); + Spicetify.Platform.LibraryAPI.getEvents()._emitter.removeListener("update", update); }; - }, []); + }, [refetch]); + const Status2 = useStatus_default(status, error); const props = { - title: data?.pages[0].openedCollection || "Albums", + title: "Albums", headerEls: [ - /* @__PURE__ */ import_react19.default.createElement(add_button_default, { - Menu: /* @__PURE__ */ import_react19.default.createElement(AddMenu, { - collection - }) + /* @__PURE__ */ import_react21.default.createElement(add_button_default, { + Menu: /* @__PURE__ */ import_react21.default.createElement(AddMenu, null) }), dropdown, - /* @__PURE__ */ import_react19.default.createElement(searchbar_default, { + /* @__PURE__ */ import_react21.default.createElement(searchbar_default, { setSearch: setTextFilter, placeholder: "Albums" }), - /* @__PURE__ */ import_react19.default.createElement(settings_button_default, { + /* @__PURE__ */ import_react21.default.createElement(settings_button_default, { configWrapper }) ] }; - if (status === "pending") { - return /* @__PURE__ */ import_react19.default.createElement(page_container_default, { + if (Status2) + return /* @__PURE__ */ import_react21.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react19.default.createElement(status_default, { - icon: "library", - heading: "Loading", - subheading: "Fetching your albums" - })); - } else if (status === "error") { - return /* @__PURE__ */ import_react19.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react19.default.createElement(status_default, { - icon: "error", - heading: "Error", - subheading: "Failed to load your albums" - })); - } else if (!data.pages[0].items.length) { - return /* @__PURE__ */ import_react19.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react19.default.createElement(status_default, { - icon: "library", - heading: "Nothing Here", - subheading: "You don't have any albums saved" - })); - } - const rootlistItems = data.pages.map((page) => page.items).flat(); - const rootlistCards = rootlistItems.map((item) => { - const isAlbum = item.type === "album"; - return /* @__PURE__ */ import_react19.default.createElement(spotify_card_default, { + }, Status2); + const contents = data; + const albums = contents.pages.flatMap((page) => page.items); + const albumCards = albums.map((item) => { + return /* @__PURE__ */ import_react21.default.createElement(spotify_card_default, { + provider: "spotify", type: item.type, uri: item.uri, header: item.name, - subheader: isAlbum ? item.artists?.[0]?.name : "Collection", - imageUrl: isAlbum ? item.images?.[0]?.url : item.imgUrl, - artistUri: isAlbum ? item.artists?.[0]?.uri : void 0 + subheader: item.artists[0].name, + imageUrl: item.images?.[0]?.url, + artistUri: item.artists[0].uri, + badge: item.pinned ? /* @__PURE__ */ import_react21.default.createElement(pin_icon_default, null) : void 0 }); }); if (hasNextPage) - rootlistCards.push(/* @__PURE__ */ import_react19.default.createElement(load_more_card_default, { + albumCards.push(/* @__PURE__ */ import_react21.default.createElement(load_more_card_default, { callback: fetchNextPage })); - return /* @__PURE__ */ import_react19.default.createElement(page_container_default, { + return /* @__PURE__ */ import_react21.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react19.default.createElement("div", { - className: `main-gridContainer-gridContainer grid` - }, rootlistCards)); + }, /* @__PURE__ */ import_react21.default.createElement("div", { + className: "main-gridContainer-gridContainer grid" + }, albumCards)); }; var albums_default = AlbumsPage; // src/pages/artists.tsx - var import_react20 = __toESM(require_react()); - var sortOptions2 = [ - { id: "0", name: "Name" }, - { id: "1", name: "Date Added" } - ]; + var import_react22 = __toESM(require_react()); var AddMenu2 = () => { const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; const { SVGIcons } = Spicetify; @@ -980,117 +1012,106 @@ var library = (() => { }; Spicetify.PopupModal.display({ title: "Add Artist", - content: /* @__PURE__ */ import_react20.default.createElement(text_input_dialog_default, { + content: /* @__PURE__ */ import_react22.default.createElement(text_input_dialog_default, { def: "", placeholder: "Artist URI", onSave }) }); }; - return /* @__PURE__ */ import_react20.default.createElement(Menu, null, /* @__PURE__ */ import_react20.default.createElement(MenuItem2, { + return /* @__PURE__ */ import_react22.default.createElement(Menu, null, /* @__PURE__ */ import_react22.default.createElement(MenuItem2, { onClick: addAlbum, - leadingIcon: /* @__PURE__ */ import_react20.default.createElement(leading_icon_default, { - path: SVGIcons["artist"] + leadingIcon: /* @__PURE__ */ import_react22.default.createElement(leading_icon_default, { + path: SVGIcons.artist }) }, "Add Artist")); }; + var limit2 = 200; + var sortOptions2 = [ + { id: "0", name: "Name" }, + { id: "1", name: "Date Added" } + ]; var ArtistsPage = ({ configWrapper }) => { const [dropdown, sortOption] = useDropdownMenu_default(sortOptions2, "library:artists"); - const [textFilter, setTextFilter] = import_react20.default.useState(""); - const { useInfiniteQuery } = Spicetify.ReactQuery; - const limit = 200; + const [textFilter, setTextFilter] = import_react22.default.useState(""); const fetchArtists = async ({ pageParam }) => { const res = await Spicetify.Platform.LibraryAPI.getContents({ filters: ["1"], sortOrder: sortOption.id, textFilter, offset: pageParam, - limit + limit: limit2 }); + if (!res.items?.length) + throw new Error("No artists found"); return res; }; - const { data, status, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ + const { data, status, error, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ queryKey: ["library:artists", sortOption.id, textFilter], queryFn: fetchArtists, initialPageParam: 0, - getNextPageParam: (lastPage, _allPages, lastPageParam) => { - return lastPage.totalLength > lastPageParam + limit ? lastPageParam + limit : void 0; + getNextPageParam: (lastPage) => { + const current = lastPage.offset + limit2; + if (lastPage.totalLength > current) + return current; } }); - import_react20.default.useEffect(() => { - const onUpdate = (e) => refetch(); - Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", onUpdate); - return () => Spicetify.Platform.LibraryAPI.getEvents()._emitter.removeListener("update", onUpdate); - }, []); + (0, import_react22.useEffect)(() => { + const update = (e) => { + if (e.data.list === "artists") + refetch(); + }; + Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", update, {}); + return () => { + Spicetify.Platform.LibraryAPI.getEvents()._emitter.removeListener("update", update); + }; + }, [refetch]); + const Status2 = useStatus_default(status, error); const props = { title: "Artists", headerEls: [ - /* @__PURE__ */ import_react20.default.createElement(add_button_default, { - Menu: /* @__PURE__ */ import_react20.default.createElement(AddMenu2, null) + /* @__PURE__ */ import_react22.default.createElement(add_button_default, { + Menu: /* @__PURE__ */ import_react22.default.createElement(AddMenu2, null) }), dropdown, - /* @__PURE__ */ import_react20.default.createElement(searchbar_default, { + /* @__PURE__ */ import_react22.default.createElement(searchbar_default, { setSearch: setTextFilter, placeholder: "Artists" }), - /* @__PURE__ */ import_react20.default.createElement(settings_button_default, { + /* @__PURE__ */ import_react22.default.createElement(settings_button_default, { configWrapper }) ] }; - if (status === "pending") { - return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { + if (Status2) + return /* @__PURE__ */ import_react22.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react20.default.createElement(status_default, { - icon: "library", - heading: "Loading", - subheading: "Fetching your artists" - })); - } else if (status === "error") { - return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react20.default.createElement(status_default, { - icon: "error", - heading: "Error", - subheading: "Failed to load your artists" - })); - } else if (!data.pages[0].items.length) { - return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react20.default.createElement(status_default, { - icon: "library", - heading: "Nothing Here", - subheading: "You don't have any artists saved" - })); - } - const artists = data.pages.map((page) => page.items).flat(); - const artistCards = artists.map((artist) => { - return /* @__PURE__ */ import_react20.default.createElement(spotify_card_default, { - type: "artist", - uri: artist.uri, - header: artist.name, - subheader: "Artist", - imageUrl: artist.images?.[0]?.url || "" - }); - }); + }, Status2); + const contents = data; + const artists = contents.pages.flatMap((page) => page.items); + const artistCards = artists.map((artist) => /* @__PURE__ */ import_react22.default.createElement(spotify_card_default, { + provider: "spotify", + type: "artist", + uri: artist.uri, + header: artist.name, + subheader: "", + imageUrl: artist.images?.at(0)?.url, + badge: artist.pinned ? /* @__PURE__ */ import_react22.default.createElement(pin_icon_default, null) : void 0 + })); if (hasNextPage) - artistCards.push(/* @__PURE__ */ import_react20.default.createElement(load_more_card_default, { + artistCards.push(/* @__PURE__ */ import_react22.default.createElement(load_more_card_default, { callback: fetchNextPage })); - return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { + return /* @__PURE__ */ import_react22.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react20.default.createElement("div", { - className: `main-gridContainer-gridContainer grid` + }, /* @__PURE__ */ import_react22.default.createElement("div", { + className: "main-gridContainer-gridContainer grid" }, artistCards)); }; var artists_default = ArtistsPage; // src/pages/shows.tsx - var import_react21 = __toESM(require_react()); - var sortOptions3 = [ - { id: "0", name: "Name" }, - { id: "1", name: "Date Added" } - ]; + var import_react23 = __toESM(require_react()); var AddMenu3 = () => { const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; const { SVGIcons } = Spicetify; @@ -1100,113 +1121,150 @@ var library = (() => { }; Spicetify.PopupModal.display({ title: "Add Show", - content: /* @__PURE__ */ import_react21.default.createElement(text_input_dialog_default, { + content: /* @__PURE__ */ import_react23.default.createElement(text_input_dialog_default, { def: "", placeholder: "Show URI", onSave }) }); }; - return /* @__PURE__ */ import_react21.default.createElement(Menu, null, /* @__PURE__ */ import_react21.default.createElement(MenuItem2, { + return /* @__PURE__ */ import_react23.default.createElement(Menu, null, /* @__PURE__ */ import_react23.default.createElement(MenuItem2, { onClick: addAlbum, - leadingIcon: /* @__PURE__ */ import_react21.default.createElement(leading_icon_default, { - path: SVGIcons["podcasts"] + leadingIcon: /* @__PURE__ */ import_react23.default.createElement(leading_icon_default, { + path: SVGIcons.podcasts }) }, "Add Show")); }; + var limit3 = 200; + var sortOptions3 = [ + { id: "0", name: "Name" }, + { id: "1", name: "Date Added" } + ]; var ShowsPage = ({ configWrapper }) => { const [dropdown, sortOption] = useDropdownMenu_default(sortOptions3, "library:shows"); - const [textFilter, setTextFilter] = import_react21.default.useState(""); - const { useInfiniteQuery } = Spicetify.ReactQuery; - const limit = 200; + const [textFilter, setTextFilter] = import_react23.default.useState(""); const fetchShows = async ({ pageParam }) => { const res = await Spicetify.Platform.LibraryAPI.getContents({ filters: ["3"], sortOrder: sortOption.id, textFilter, offset: pageParam, - limit + limit: limit3 }); + if (!res.items?.length) + throw new Error("No shows found"); return res; }; - const { data, status, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ + const { data, status, error, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ queryKey: ["library:shows", sortOption.id, textFilter], queryFn: fetchShows, initialPageParam: 0, - getNextPageParam: (lastPage, _allPages, lastPageParam) => { - return lastPage.totalLength > lastPageParam + limit ? lastPageParam + limit : void 0; + getNextPageParam: (lastPage) => { + const current = lastPage.offset + limit3; + if (lastPage.totalLength > current) + return current; } }); - import_react21.default.useEffect(() => { - const onUpdate = (e) => refetch(); - Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", onUpdate); - return () => Spicetify.Platform.LibraryAPI.getEvents()._emitter.removeListener("update", onUpdate); - }, []); + (0, import_react23.useEffect)(() => { + const update = (e) => { + if (e.data.list === "shows") + refetch(); + }; + Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", update, {}); + return () => { + Spicetify.Platform.LibraryAPI.getEvents()._emitter.removeListener("update", update); + }; + }, [refetch]); + const Status2 = useStatus_default(status, error); const props = { title: "Shows", headerEls: [ - /* @__PURE__ */ import_react21.default.createElement(add_button_default, { - Menu: /* @__PURE__ */ import_react21.default.createElement(AddMenu3, null) + /* @__PURE__ */ import_react23.default.createElement(add_button_default, { + Menu: /* @__PURE__ */ import_react23.default.createElement(AddMenu3, null) }), dropdown, - /* @__PURE__ */ import_react21.default.createElement(searchbar_default, { + /* @__PURE__ */ import_react23.default.createElement(searchbar_default, { setSearch: setTextFilter, placeholder: "Shows" }), - /* @__PURE__ */ import_react21.default.createElement(settings_button_default, { + /* @__PURE__ */ import_react23.default.createElement(settings_button_default, { configWrapper }) ] }; - if (status === "pending") { - return /* @__PURE__ */ import_react21.default.createElement(page_container_default, { + if (Status2) + return /* @__PURE__ */ import_react23.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react21.default.createElement(status_default, { - icon: "library", - heading: "Loading", - subheading: "Fetching your shows" - })); - } else if (status === "error") { - return /* @__PURE__ */ import_react21.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react21.default.createElement(status_default, { - icon: "error", - heading: "Error", - subheading: "Failed to load your shows" - })); - } else if (!data.pages[0].items.length) { - return /* @__PURE__ */ import_react21.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react21.default.createElement(status_default, { - icon: "library", - heading: "Nothing Here", - subheading: "You don't have any shows saved" - })); - } - const shows = data.pages.map((page) => page.items).flat(); - const showCards = shows.map((show) => { - return /* @__PURE__ */ import_react21.default.createElement(spotify_card_default, { - type: "show", - uri: show.uri, - header: show.name, - subheader: show.publisher, - imageUrl: show.images?.[0]?.url || "" - }); - }); + }, Status2); + const contents = data; + const shows = contents.pages.flatMap((page) => page.items); + const showCards = shows.map((show) => /* @__PURE__ */ import_react23.default.createElement(spotify_card_default, { + provider: "spotify", + type: "show", + uri: show.uri, + header: show.name, + subheader: show.publisher, + imageUrl: show.images?.[0]?.url, + badge: show.pinned ? /* @__PURE__ */ import_react23.default.createElement(pin_icon_default, null) : void 0 + })); if (hasNextPage) - showCards.push(/* @__PURE__ */ import_react21.default.createElement(load_more_card_default, { + showCards.push(/* @__PURE__ */ import_react23.default.createElement(load_more_card_default, { callback: fetchNextPage })); - return /* @__PURE__ */ import_react21.default.createElement(page_container_default, { + return /* @__PURE__ */ import_react23.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react21.default.createElement("div", { - className: `main-gridContainer-gridContainer grid` + }, /* @__PURE__ */ import_react23.default.createElement("div", { + className: "main-gridContainer-gridContainer grid" }, showCards)); }; var shows_default = ShowsPage; // src/pages/playlists.tsx - var import_react22 = __toESM(require_react()); + var import_react24 = __toESM(require_react()); + var AddMenu4 = ({ folder }) => { + const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; + const { RootlistAPI } = Spicetify.Platform; + const { SVGIcons } = Spicetify; + const insertLocation = folder ? { uri: folder } : "start"; + const createFolder = () => { + const onSave = (value) => { + RootlistAPI.createFolder(value || "New Folder", { after: insertLocation }); + }; + Spicetify.PopupModal.display({ + title: "Create Folder", + content: /* @__PURE__ */ import_react24.default.createElement(text_input_dialog_default, { + def: "New Folder", + placeholder: "Folder Name", + onSave + }) + }); + }; + const createPlaylist = () => { + const onSave = (value) => { + RootlistAPI.createPlaylist(value || "New Playlist", { after: insertLocation }); + }; + Spicetify.PopupModal.display({ + title: "Create Playlist", + content: /* @__PURE__ */ import_react24.default.createElement(text_input_dialog_default, { + def: "New Playlist", + placeholder: "Playlist Name", + onSave + }) + }); + }; + return /* @__PURE__ */ import_react24.default.createElement(Menu, null, /* @__PURE__ */ import_react24.default.createElement(MenuItem2, { + onClick: createFolder, + leadingIcon: /* @__PURE__ */ import_react24.default.createElement(leading_icon_default, { + path: SVGIcons["playlist-folder"] + }) + }, "Create Folder"), /* @__PURE__ */ import_react24.default.createElement(MenuItem2, { + onClick: createPlaylist, + leadingIcon: /* @__PURE__ */ import_react24.default.createElement(leading_icon_default, { + path: SVGIcons.playlist + }) + }, "Create Playlist")); + }; + var limit4 = 200; var dropdownOptions = [ { id: "0", name: "Name" }, { id: "1", name: "Date Added" }, @@ -1220,56 +1278,16 @@ var library = (() => { { id: "102", name: "By You" }, { id: "103", name: "By Spotify" } ]; - var AddMenu4 = ({ folder }) => { - const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; - const { RootlistAPI } = Spicetify.Platform; - const { SVGIcons } = Spicetify; - const insertLocation = folder ? { uri: folder } : "start"; - const createFolder = () => { - const onSave = (value) => { - RootlistAPI.createFolder(value || "New Folder", { after: insertLocation }); - }; - Spicetify.PopupModal.display({ - title: "Create Folder", - content: /* @__PURE__ */ import_react22.default.createElement(text_input_dialog_default, { - def: "New Folder", - placeholder: "Folder Name", - onSave - }) - }); - }; - const createPlaylist = () => { - const onSave = (value) => { - RootlistAPI.createPlaylist(value || "New Playlist", { after: insertLocation }); - }; - Spicetify.PopupModal.display({ - title: "Create Playlist", - content: /* @__PURE__ */ import_react22.default.createElement(text_input_dialog_default, { - def: "New Playlist", - placeholder: "Playlist Name", - onSave - }) - }); - }; - return /* @__PURE__ */ import_react22.default.createElement(Menu, null, /* @__PURE__ */ import_react22.default.createElement(MenuItem2, { - onClick: createFolder, - leadingIcon: /* @__PURE__ */ import_react22.default.createElement(leading_icon_default, { - path: SVGIcons["playlist-folder"] - }) - }, "Create Folder"), /* @__PURE__ */ import_react22.default.createElement(MenuItem2, { - onClick: createPlaylist, - leadingIcon: /* @__PURE__ */ import_react22.default.createElement(leading_icon_default, { - path: SVGIcons["playlist"] - }) - }, "Create Playlist")); - }; + var flattenOptions = [ + { id: "false", name: "Unflattened" }, + { id: "true", name: "Flattened" } + ]; var PlaylistsPage = ({ folder, configWrapper }) => { const [sortDropdown, sortOption] = useDropdownMenu_default(dropdownOptions, "library:playlists-sort"); - const [filterDropdown, filterOption, setFilterOption, setAvailableOptions] = useDropdownMenu_default(filterOptions); - const [textFilter, setTextFilter] = import_react22.default.useState(""); - const [images, setImages] = import_react22.default.useState({ ...SpicetifyLibrary.FolderImageWrapper.getFolderImages() }); - const { useInfiniteQuery } = Spicetify.ReactQuery; - const limit = 200; + const [filterDropdown, filterOption] = useDropdownMenu_default(filterOptions); + const [flattenDropdown, flattenOption] = useDropdownMenu_default(flattenOptions); + const [textFilter, setTextFilter] = import_react24.default.useState(""); + const [images, setImages] = import_react24.default.useState({ ...FolderImageWrapper.getFolderImages() }); const fetchRootlist = async ({ pageParam }) => { const filters = filterOption.id === "all" ? ["2"] : ["2", filterOption.id]; const res = await Spicetify.Platform.LibraryAPI.getContents({ @@ -1278,99 +1296,229 @@ var library = (() => { folderUri: folder, textFilter, offset: pageParam, - limit + limit: limit4, + flattenTree: JSON.parse(flattenOption.id) }); + if (!res.items?.length) + throw new Error("No playlists found"); return res; }; - const { data, status, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ - queryKey: ["library:playlists", sortOption.id, filterOption.id, textFilter, folder], + const { data, status, error, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ + queryKey: ["library:playlists", sortOption.id, filterOption.id, flattenOption.id, textFilter, folder], queryFn: fetchRootlist, initialPageParam: 0, - getNextPageParam: (lastPage, _allPages, lastPageParam) => { - return lastPage.totalLength > lastPageParam + limit ? lastPageParam + limit : void 0; - } + getNextPageParam: (lastPage) => { + const current = lastPage.offset + limit4; + if (lastPage.totalLength > current) + return current; + }, + retry: false }); - import_react22.default.useEffect(() => { - const onUpdate = (e) => refetch(); - const onImageUpdate = (e) => setImages({ ...e.detail }); - Spicetify.Platform.RootlistAPI.getEvents().addListener("update", onUpdate); - SpicetifyLibrary.FolderImageWrapper.addEventListener("update", onImageUpdate); + (0, import_react24.useEffect)(() => { + const update = (e) => refetch(); + const updateImages = (e) => "detail" in e && setImages({ ...e.detail }); + FolderImageWrapper.addEventListener("update", updateImages); + Spicetify.Platform.RootlistAPI.getEvents()._emitter.addListener("update", update, {}); return () => { - Spicetify.Platform.RootlistAPI.getEvents().removeListener("update", onUpdate); - SpicetifyLibrary.FolderImageWrapper.removeEventListener("update", onImageUpdate); + FolderImageWrapper.removeEventListener("update", updateImages); + Spicetify.Platform.RootlistAPI.getEvents()._emitter.removeListener("update", update); }; - }, []); + }, [refetch]); + const Status2 = useStatus_default(status, error); const props = { title: data?.pages[0].openedFolderName || "Playlists", headerEls: [ - /* @__PURE__ */ import_react22.default.createElement(add_button_default, { - Menu: /* @__PURE__ */ import_react22.default.createElement(AddMenu4, { + /* @__PURE__ */ import_react24.default.createElement(add_button_default, { + Menu: /* @__PURE__ */ import_react24.default.createElement(AddMenu4, { folder }) }), sortDropdown, filterDropdown, - /* @__PURE__ */ import_react22.default.createElement(searchbar_default, { + flattenDropdown, + /* @__PURE__ */ import_react24.default.createElement(searchbar_default, { setSearch: setTextFilter, placeholder: "Playlists" }), - /* @__PURE__ */ import_react22.default.createElement(settings_button_default, { + /* @__PURE__ */ import_react24.default.createElement(settings_button_default, { configWrapper }) ] }; - if (status === "pending") { - return /* @__PURE__ */ import_react22.default.createElement(page_container_default, { + if (Status2) + return /* @__PURE__ */ import_react24.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react22.default.createElement(status_default, { - icon: "library", - heading: "Loading", - subheading: "Fetching your playlists" - })); - } else if (status === "error") { - return /* @__PURE__ */ import_react22.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react22.default.createElement(status_default, { - icon: "error", - heading: "Error", - subheading: "Failed to load your playlists" - })); - } else if (!data.pages[0].items.length) { - return /* @__PURE__ */ import_react22.default.createElement(page_container_default, { - ...props - }, /* @__PURE__ */ import_react22.default.createElement(status_default, { - icon: "library", - heading: "Nothing Here", - subheading: "You don't have any playlists saved" - })); - } - const rootlistItems = data.pages.map((page) => page.items).flat(); - const rootlistCards = rootlistItems.map((playlist) => { - return /* @__PURE__ */ import_react22.default.createElement(spotify_card_default, { - type: playlist.type, - uri: playlist.uri, - header: playlist.name, - subheader: playlist.owner?.name || "Folder", - imageUrl: playlist.images?.[0]?.url || images[playlist.uri] || "" - }); - }); + }, Status2); + const contents = data; + const items = contents.pages.flatMap((page) => page.items); + const rootlistCards = items.map((item) => /* @__PURE__ */ import_react24.default.createElement(spotify_card_default, { + provider: "spotify", + type: item.type, + uri: item.uri, + header: item.name, + subheader: item.type === "playlist" ? item.owner.name : `${item.numberOfPlaylists} Playlists${item.numberOfFolders ? ` \u2022 ${item.numberOfFolders} Folders` : ""}`, + imageUrl: item.images?.[0]?.url || images[item.uri], + badge: item.pinned ? /* @__PURE__ */ import_react24.default.createElement(pin_icon_default, null) : void 0 + })); if (hasNextPage) - rootlistCards.push(/* @__PURE__ */ import_react22.default.createElement(load_more_card_default, { + rootlistCards.push(/* @__PURE__ */ import_react24.default.createElement(load_more_card_default, { callback: fetchNextPage })); - return /* @__PURE__ */ import_react22.default.createElement(page_container_default, { + return /* @__PURE__ */ import_react24.default.createElement(page_container_default, { ...props - }, /* @__PURE__ */ import_react22.default.createElement("div", { - className: `main-gridContainer-gridContainer grid` + }, /* @__PURE__ */ import_react24.default.createElement("div", { + className: "main-gridContainer-gridContainer grid" }, rootlistCards)); }; var playlists_default = PlaylistsPage; // package.json - var version = "0.1.1"; + var version = "1.0.0"; + + // src/pages/collections.tsx + var import_react25 = __toESM(require_react()); + var AddMenu5 = ({ collection }) => { + const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; + const { RootlistAPI } = Spicetify.Platform; + const { SVGIcons } = Spicetify; + const createCollection = () => { + const onSave = (value) => { + CollectionsWrapper.createCollection(value || "New Collection", collection); + }; + Spicetify.PopupModal.display({ + title: "Create Collection", + content: /* @__PURE__ */ import_react25.default.createElement(text_input_dialog_default, { + def: "New Collection", + placeholder: "Collection Name", + onSave + }) + }); + }; + const createDiscogCollection = () => { + const onSave = (value) => { + CollectionsWrapper.createCollectionFromDiscog(value); + }; + Spicetify.PopupModal.display({ + title: "Create Discog Collection", + content: /* @__PURE__ */ import_react25.default.createElement(text_input_dialog_default, { + def: "", + placeholder: "Artist URI", + onSave + }) + }); + }; + const addAlbum = () => { + if (!collection) + return; + const onSave = (value) => { + CollectionsWrapper.addAlbumToCollection(collection, value); + }; + Spicetify.PopupModal.display({ + title: "Add Album", + content: /* @__PURE__ */ import_react25.default.createElement(text_input_dialog_default, { + def: "", + placeholder: "Album URI", + onSave + }) + }); + }; + return /* @__PURE__ */ import_react25.default.createElement(Menu, null, /* @__PURE__ */ import_react25.default.createElement(MenuItem2, { + onClick: createCollection, + leadingIcon: /* @__PURE__ */ import_react25.default.createElement(leading_icon_default, { + path: SVGIcons["playlist-folder"] + }) + }, "Create Collection"), /* @__PURE__ */ import_react25.default.createElement(MenuItem2, { + onClick: createDiscogCollection, + leadingIcon: /* @__PURE__ */ import_react25.default.createElement(leading_icon_default, { + path: SVGIcons.artist + }) + }, "Create Discog Collection"), collection && /* @__PURE__ */ import_react25.default.createElement(MenuItem2, { + onClick: addAlbum, + leadingIcon: /* @__PURE__ */ import_react25.default.createElement(leading_icon_default, { + path: SVGIcons.album + }) + }, "Add Album")); + }; + var limit5 = 200; + var CollectionsPage = ({ collection, configWrapper }) => { + const [textFilter, setTextFilter] = import_react25.default.useState(""); + const fetchRootlist = async ({ pageParam }) => { + const res = await CollectionsWrapper.getContents({ + collectionUri: collection, + textFilter, + offset: pageParam, + limit: limit5 + }); + if (!res.items.length) + throw new Error("No collections found"); + return res; + }; + const { data, status, error, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ + queryKey: ["library:collections", textFilter, collection], + queryFn: fetchRootlist, + initialPageParam: 0, + getNextPageParam: (lastPage) => { + const current = lastPage.offset + limit5; + if (lastPage.totalLength > current) + return current; + }, + retry: false, + structuralSharing: false + }); + (0, import_react25.useEffect)(() => { + const update = (e) => { + refetch(); + }; + CollectionsWrapper.addEventListener("update", update); + return () => { + CollectionsWrapper.removeEventListener("update", update); + }; + }, [refetch]); + const Status2 = useStatus_default(status, error); + const props = { + title: data?.pages[0].openedCollectionName || "Collections", + headerEls: [ + /* @__PURE__ */ import_react25.default.createElement(add_button_default, { + Menu: /* @__PURE__ */ import_react25.default.createElement(AddMenu5, { + collection + }) + }), + /* @__PURE__ */ import_react25.default.createElement(searchbar_default, { + setSearch: setTextFilter, + placeholder: "Collections" + }), + /* @__PURE__ */ import_react25.default.createElement(settings_button_default, { + configWrapper + }) + ] + }; + if (Status2) + return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { + ...props + }, Status2); + const contents = data; + const items = contents.pages.flatMap((page) => page.items); + const rootlistCards = items.map((item) => /* @__PURE__ */ import_react25.default.createElement(spotify_card_default, { + provider: "spotify", + type: item.type, + uri: item.uri, + header: item.name, + subheader: item.type === "collection" ? `${item.items.length} Albums` : item.artists?.[0]?.name, + imageUrl: item.type === "collection" ? item.image : item.images?.[0]?.url + })); + if (hasNextPage) + rootlistCards.push(/* @__PURE__ */ import_react25.default.createElement(load_more_card_default, { + callback: fetchNextPage + })); + return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react25.default.createElement("div", { + className: "main-gridContainer-gridContainer grid" + }, rootlistCards)); + }; + var collections_default = CollectionsPage; // src/app.tsx - var tabPages = ["Playlists", "Albums", "Artists", "Shows"]; var checkForUpdates = (setNewUpdate) => { fetch("https://api.github.com/repos/harbassan/spicetify-apps/releases").then((res) => res.json()).then( (result) => { @@ -1384,40 +1532,46 @@ var library = (() => { }; var NavbarContainer = ({ configWrapper }) => { const pages = { - ["Artists"]: /* @__PURE__ */ import_react23.default.createElement(artists_default, { + ["Artists"]: /* @__PURE__ */ import_react26.default.createElement(artists_default, { configWrapper }), - ["Albums"]: /* @__PURE__ */ import_react23.default.createElement(albums_default, { + ["Albums"]: /* @__PURE__ */ import_react26.default.createElement(albums_default, { configWrapper }), - ["Shows"]: /* @__PURE__ */ import_react23.default.createElement(shows_default, { + ["Shows"]: /* @__PURE__ */ import_react26.default.createElement(shows_default, { configWrapper }), - ["Playlists"]: /* @__PURE__ */ import_react23.default.createElement(playlists_default, { + ["Playlists"]: /* @__PURE__ */ import_react26.default.createElement(playlists_default, { + configWrapper + }), + ["Collections"]: /* @__PURE__ */ import_react26.default.createElement(collections_default, { configWrapper }) }; + const tabPages = ["Playlists", "Albums", "Collections", "Artists", "Shows"].filter( + (page) => configWrapper.config[`show-${page.toLowerCase()}`] + ); const [navBar, activeLink, setActiveLink] = useNavigationBar_default(tabPages); - const [firstUpdate, setFirstUpdate] = import_react23.default.useState(true); - const [newUpdate, setNewUpdate] = import_react23.default.useState(false); - import_react23.default.useEffect(() => { + const [firstUpdate, setFirstUpdate] = import_react26.default.useState(true); + const [newUpdate, setNewUpdate] = import_react26.default.useState(false); + import_react26.default.useEffect(() => { setActiveLink(Spicetify.LocalStorage.get("library:active-link") || "Playlists"); checkForUpdates(setNewUpdate); setFirstUpdate(false); }, []); - import_react23.default.useEffect(() => { + import_react26.default.useEffect(() => { Spicetify.LocalStorage.set("library:active-link", activeLink); }, [activeLink]); if (firstUpdate) - return /* @__PURE__ */ import_react23.default.createElement(import_react23.default.Fragment, null); - return /* @__PURE__ */ import_react23.default.createElement(import_react23.default.Fragment, null, navBar, newUpdate && /* @__PURE__ */ import_react23.default.createElement("div", { + return /* @__PURE__ */ import_react26.default.createElement(import_react26.default.Fragment, null); + return /* @__PURE__ */ import_react26.default.createElement(import_react26.default.Fragment, null, navBar, newUpdate && /* @__PURE__ */ import_react26.default.createElement("div", { className: "new-update" - }, "New app update available! Visit", " ", /* @__PURE__ */ import_react23.default.createElement("a", { + }, "New app update available! Visit", " ", /* @__PURE__ */ import_react26.default.createElement("a", { href: "https://github.com/harbassan/spicetify-apps/releases" }, "harbassan/spicetify-apps"), " to install."), pages[activeLink]); }; var App = () => { - const [config, setConfig] = import_react23.default.useState({ ...SpicetifyLibrary.ConfigWrapper.Config }); + const [config, setConfig] = import_react26.default.useState({ ...SpicetifyLibrary.ConfigWrapper.Config }); const launchModal = () => { SpicetifyLibrary.ConfigWrapper.launchModal(setConfig); }; @@ -1428,33 +1582,33 @@ var library = (() => { const { pathname } = Spicetify.Platform.History.location; const route = pathname.slice(8); if (/^\/folder\/.+/.test(route)) { - return /* @__PURE__ */ import_react23.default.createElement("div", { + return /* @__PURE__ */ import_react26.default.createElement("div", { id: "library-app" - }, /* @__PURE__ */ import_react23.default.createElement(playlists_default, { + }, /* @__PURE__ */ import_react26.default.createElement(playlists_default, { folder: route.split("/").pop(), configWrapper })); } if (/^\/collection\/.+/.test(route)) { - return /* @__PURE__ */ import_react23.default.createElement("div", { + return /* @__PURE__ */ import_react26.default.createElement("div", { id: "library-app" - }, /* @__PURE__ */ import_react23.default.createElement(albums_default, { + }, /* @__PURE__ */ import_react26.default.createElement(collections_default, { collection: route.split("/").pop(), configWrapper })); } - return /* @__PURE__ */ import_react23.default.createElement("div", { + return /* @__PURE__ */ import_react26.default.createElement("div", { id: "library-app" - }, /* @__PURE__ */ import_react23.default.createElement(NavbarContainer, { + }, /* @__PURE__ */ import_react26.default.createElement(NavbarContainer, { configWrapper })); }; var app_default = App; // ../../../AppData/Local/Temp/spicetify-creator/index.jsx - var import_react24 = __toESM(require_react()); + var import_react27 = __toESM(require_react()); function render() { - return /* @__PURE__ */ import_react24.default.createElement(app_default, null); + return /* @__PURE__ */ import_react27.default.createElement(app_default, null); } return __toCommonJS(spicetify_creator_exports); })(); diff --git a/.config/spicetify/CustomApps/library/style.css b/.config/spicetify/CustomApps/library/style.css index 9a15dd6c..9df462b7 100644 --- a/.config/spicetify/CustomApps/library/style.css +++ b/.config/spicetify/CustomApps/library/style.css @@ -1,4 +1,4 @@ -/* ../../../AppData/Local/Temp/tmp-5152-1Ex7Bvh4Tu6u/18f3cb8bc8b4/navBar.module.css */ +/* ../../../AppData/Local/Temp/tmp-4464-wbZ6O1BKhuot/19178f16f304/navBar.module.css */ .navBar-module__topBarHeaderItem___piw4C_library { -webkit-app-region: no-drag; display: inline-block; @@ -46,7 +46,7 @@ div.navBar-module__topBarHeaderItemLink___xA4uv_library { padding: 0; } -/* ../../../AppData/Local/Temp/tmp-5152-1Ex7Bvh4Tu6u/18f3cb8baff0/app.css */ +/* ../../../AppData/Local/Temp/tmp-4464-wbZ6O1BKhuot/19178f16dc80/app.css */ :root { --library-card-size: 180px; --library-searchbar-size: 200px; @@ -57,6 +57,10 @@ div.navBar-module__topBarHeaderItemLink___xA4uv_library { #library-app .grid { grid-template-columns: repeat(auto-fill, minmax(var(--library-card-size), 1fr)) !important; } +#library-app .main-card-cardContainer { + width: 100%; + height: 100%; +} #library-app .load-more-card { display: flex; gap: 10px; @@ -100,7 +104,7 @@ div.navBar-module__topBarHeaderItemLink___xA4uv_library { align-self: end; } -/* ../../../AppData/Local/Temp/tmp-5152-1Ex7Bvh4Tu6u/18f3cb8bc361/external.css */ +/* ../../../AppData/Local/Temp/tmp-4464-wbZ6O1BKhuot/19178f16ee21/external.css */ body:not(.show-ylx-filters) .main-yourLibraryX-filterArea:not(:has(> .main-yourLibraryX-libraryFilter)), .main-yourLibraryX-header:not(:has(> .main-yourLibraryX-headerContent > .main-yourLibraryX-collapseButton > button:nth-child(2))), .main-yourLibraryX-collapseButton > button:first-child, @@ -113,10 +117,12 @@ body:not(.show-ylx-filters) .main-yourLibraryX-filterArea:not(:has(> .main-yourL .main-yourLibraryX-header { margin-top: -8px; } -.main-yourLibraryX-librarySortWrapper button span:first-child { +.main-yourLibraryX-libraryFilter .main-yourLibraryX-librarySortWrapper button span:first-child, +.main-yourLibraryX-libraryFilter span[role=presentation] span[role=presentation] button span:first-child { display: none; } -.main-yourLibraryX-librarySortWrapper { +.main-yourLibraryX-libraryFilter .main-yourLibraryX-librarySortWrapper, +.main-yourLibraryX-libraryFilter span[role=presentation] { margin-left: auto; } .toggle-filters-button > button:after, @@ -140,21 +146,24 @@ li.main-yourLibraryX-navItem[data-id="/library"] { li.main-yourLibraryX-navItem[data-id="/library"] > a { flex-grow: 1; } -.toggle-filters-button > button, -.collapse-button > button, -.main-yourLibraryX-librarySortWrapper > button { +.main-yourLibraryX-libraryFilter .toggle-filters-button > button, +.main-yourLibraryX-libraryFilter .collapse-button > button, +.main-yourLibraryX-libraryFilter .main-yourLibraryX-librarySortWrapper > button, +.main-yourLibraryX-libraryFilter span[role=presentation] span[role=presentation] > button { padding: 0; } -.toggle-filters-button, -.collapse-button, -.main-yourLibraryX-librarySortWrapper { +.main-yourLibraryX-libraryFilter .toggle-filters-button, +.main-yourLibraryX-libraryFilter .collapse-button, +.main-yourLibraryX-libraryFilter .main-yourLibraryX-librarySortWrapper, +.main-yourLibraryX-libraryFilter span[role=presentation] span[role=presentation] { display: flex; flex-basis: 32px; justify-content: center; min-width: 24px; flex-shrink: 10; } -.main-yourLibraryX-librarySortWrapper > button > span:nth-child(2) { +.main-yourLibraryX-libraryFilter .main-yourLibraryX-librarySortWrapper > button > span:nth-child(2), +.main-yourLibraryX-libraryFilter span[role=presentation] span[role=presentation] > button > span:nth-child(2) { margin: 0; } .LayoutResizer__resize-bar { @@ -227,7 +236,7 @@ li.main-yourLibraryX-navItem[data-id="/library"] > a { transform: scale(1.04); } -/* ../../../AppData/Local/Temp/tmp-5152-1Ex7Bvh4Tu6u/18f3cb8bc5f2/config_modal.css */ +/* ../../../AppData/Local/Temp/tmp-4464-wbZ6O1BKhuot/19178f16f0a2/config_modal.css */ .config-container { gap: 10px; display: flex; @@ -304,7 +313,7 @@ li.main-yourLibraryX-navItem[data-id="/library"] > a { width: 200px; } -/* ../../../AppData/Local/Temp/tmp-5152-1Ex7Bvh4Tu6u/18f3cb8bc713/shared.css */ +/* ../../../AppData/Local/Temp/tmp-4464-wbZ6O1BKhuot/19178f16f163/shared.css */ .grid { --grid-gap: 24px; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important; @@ -327,6 +336,20 @@ li.main-yourLibraryX-navItem[data-id="/library"] > a { flex-direction: column; gap: 24px; } +.badge { + position: absolute; + top: 3%; + left: 3%; + height: 30px; + width: 30px; + border-radius: 50%; + background-color: rgb(65, 110, 170); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; +} .page-header { align-content: space-between; align-items: center; diff --git a/.config/spicetify/config-xpui.ini b/.config/spicetify/config-xpui.ini index 585d0df1..4032e9aa 100644 --- a/.config/spicetify/config-xpui.ini +++ b/.config/spicetify/config-xpui.ini @@ -29,4 +29,4 @@ custom_apps = library|lyrics-plus|marketplace|stats ; DO NOT CHANGE! [Backup] version = 1.2.42.290.g242057a2 -with = 2.37.1 +with = 2.38.3