diff options
Diffstat (limited to 'dot-config/spicetify/CustomApps/stats/index.js')
| -rw-r--r-- | dot-config/spicetify/CustomApps/stats/index.js | 2484 |
1 files changed, 2484 insertions, 0 deletions
diff --git a/dot-config/spicetify/CustomApps/stats/index.js b/dot-config/spicetify/CustomApps/stats/index.js new file mode 100644 index 00000000..56fcfc30 --- /dev/null +++ b/dot-config/spicetify/CustomApps/stats/index.js @@ -0,0 +1,2484 @@ +var stats = (() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod + )); + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + + // external-global-plugin:react + var require_react = __commonJS({ + "external-global-plugin:react"(exports, module) { + module.exports = Spicetify.React; + } + }); + + // external-global-plugin:react-dom + var require_react_dom = __commonJS({ + "external-global-plugin:react-dom"(exports, module) { + module.exports = Spicetify.ReactDOM; + } + }); + + // ../../../AppData/Local/Temp/spicetify-creator/index.jsx + var spicetify_creator_exports = {}; + __export(spicetify_creator_exports, { + default: () => render + }); + + // src/app.tsx + var import_react29 = __toESM(require_react()); + + // ../node_modules/spcr-navigation-bar/useNavigationBar.tsx + var import_react3 = __toESM(require_react()); + + // ../node_modules/spcr-navigation-bar/navBar.tsx + var import_react2 = __toESM(require_react()); + var import_react_dom = __toESM(require_react_dom()); + + // ../node_modules/spcr-navigation-bar/optionsMenu.tsx + var import_react = __toESM(require_react()); + var OptionsMenuItemIcon = /* @__PURE__ */ import_react.default.createElement("svg", { + width: 16, + height: 16, + viewBox: "0 0 16 16", + fill: "currentColor" + }, /* @__PURE__ */ import_react.default.createElement("path", { + d: "M13.985 2.383L5.127 12.754 1.388 8.375l-.658.77 4.397 5.149 9.618-11.262z" + })); + var OptionsMenuItem = import_react.default.memo((props) => { + return /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.MenuItem, { + onClick: props.onSelect, + icon: props.isSelected ? OptionsMenuItemIcon : null + }, props.value); + }); + var OptionsMenu = import_react.default.memo((props) => { + const menuRef = import_react.default.useRef(null); + const menu = /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.Menu, null, props.options.map( + (option) => /* @__PURE__ */ import_react.default.createElement(OptionsMenuItem, { + value: option.link, + isSelected: option.isActive, + onSelect: () => { + props.onSelect(option.link); + menuRef.current?.click(); + } + }) + )); + return /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.ContextMenu, { + menu, + trigger: "click", + action: "toggle", + renderInLine: true + }, /* @__PURE__ */ import_react.default.createElement("button", { + className: navBar_module_default.optionsMenuDropBox, + ref: menuRef + }, /* @__PURE__ */ import_react.default.createElement("span", { + className: props.bold ? "main-type-mestoBold" : "main-type-mesto" + }, props.options.find((o) => o.isActive)?.link || props.defaultValue), /* @__PURE__ */ import_react.default.createElement("svg", { + width: 16, + height: 16, + viewBox: "0 0 16 16", + fill: "currentColor" + }, /* @__PURE__ */ import_react.default.createElement("path", { + d: "M3 6l5 5.794L13 6z" + })))); + }); + var optionsMenu_default = OptionsMenu; + + // postcss-module:C:\Users\user\AppData\Local\Temp\tmp-17120-n3iWRzissBjg\18f3cbbb4bc3\navBar.module.css + var navBar_module_default = { "topBarHeaderItem": "navBar-module__topBarHeaderItem___piw4C_stats", "topBarHeaderItemLink": "navBar-module__topBarHeaderItemLink___xA4uv_stats", "topBarActive": "navBar-module__topBarActive___XhWpm_stats", "topBarNav": "navBar-module__topBarNav___qWGeZ_stats", "optionsMenuDropBox": "navBar-module__optionsMenuDropBox___pzfNI_stats" }; + + // ../node_modules/spcr-navigation-bar/navBar.tsx + var NavbarItem2 = class { + constructor(link, isActive) { + this.link = link; + this.isActive = isActive; + } + }; + var NavbarItemComponent = (props) => { + return /* @__PURE__ */ import_react2.default.createElement("li", { + className: navBar_module_default.topBarHeaderItem, + onClick: (e) => { + e.preventDefault(); + props.switchTo(props.item.link); + } + }, /* @__PURE__ */ import_react2.default.createElement("a", { + className: `${navBar_module_default.topBarHeaderItemLink} ${props.item.isActive ? navBar_module_default.topBarActive : ""}`, + "aria-current": "page", + draggable: false, + href: "" + }, /* @__PURE__ */ import_react2.default.createElement("span", { + className: "main-type-mestoBold" + }, props.item.link))); + }; + var NavbarMore = import_react2.default.memo(({ items, switchTo }) => { + return /* @__PURE__ */ import_react2.default.createElement("li", { + className: `${navBar_module_default.topBarHeaderItem} ${items.find((item) => item.isActive) ? navBar_module_default.topBarActive : ""}` + }, /* @__PURE__ */ import_react2.default.createElement(optionsMenu_default, { + options: items, + onSelect: switchTo, + defaultValue: "More", + bold: true + })); + }); + var NavbarContent = (props) => { + const resizeHost = document.querySelector(".Root__main-view .os-resize-observer-host") ?? document.querySelector(".Root__main-view .os-size-observer"); + const [windowSize, setWindowSize] = (0, import_react2.useState)(resizeHost.clientWidth); + const resizeHandler = () => setWindowSize(resizeHost.clientWidth); + (0, import_react2.useEffect)(() => { + const observer = new ResizeObserver(resizeHandler); + observer.observe(resizeHost); + return () => { + observer.disconnect(); + }; + }, [resizeHandler]); + return /* @__PURE__ */ import_react2.default.createElement(NavbarContext, null, /* @__PURE__ */ import_react2.default.createElement(Navbar, { + ...props, + windowSize + })); + }; + var NavbarContext = (props) => { + return import_react_dom.default.createPortal( + /* @__PURE__ */ import_react2.default.createElement("div", { + className: "main-topbar-topbarContent" + }, props.children), + document.querySelector(".main-topBar-topbarContentWrapper") + ); + }; + var Navbar = (props) => { + const navBarListRef = import_react2.default.useRef(null); + const [childrenSizes, setChildrenSizes] = (0, import_react2.useState)([]); + const [availableSpace, setAvailableSpace] = (0, import_react2.useState)(0); + const [outOfRangeItemIndexes, setOutOfRangeItemIndexes] = (0, import_react2.useState)([]); + let items = props.links.map((link) => new NavbarItem2(link, link === props.activeLink)); + (0, import_react2.useEffect)(() => { + if (!navBarListRef.current) + return; + const children = Array.from(navBarListRef.current.children); + const navBarItemSizes = children.map((child) => child.clientWidth); + setChildrenSizes(navBarItemSizes); + }, []); + (0, import_react2.useEffect)(() => { + if (!navBarListRef.current) + return; + setAvailableSpace(navBarListRef.current.clientWidth); + }, [props.windowSize]); + (0, import_react2.useEffect)(() => { + if (!navBarListRef.current) + return; + let totalSize = childrenSizes.reduce((a, b) => a + b, 0); + if (totalSize <= availableSpace) { + setOutOfRangeItemIndexes([]); + return; + } + const viewMoreButtonSize = Math.max(...childrenSizes); + const itemsToHide = []; + let stopWidth = viewMoreButtonSize; + childrenSizes.forEach((childWidth, i) => { + if (availableSpace >= stopWidth + childWidth) { + stopWidth += childWidth; + } else if (i !== items.length) { + itemsToHide.push(i); + } + }); + setOutOfRangeItemIndexes(itemsToHide); + }, [availableSpace, childrenSizes]); + return /* @__PURE__ */ import_react2.default.createElement("nav", { + className: navBar_module_default.topBarNav + }, /* @__PURE__ */ import_react2.default.createElement("ul", { + className: navBar_module_default.topBarHeader, + ref: navBarListRef + }, items.filter((_, id) => !outOfRangeItemIndexes.includes(id)).map( + (item) => /* @__PURE__ */ import_react2.default.createElement(NavbarItemComponent, { + item, + switchTo: props.switchCallback + }) + ), outOfRangeItemIndexes.length ? /* @__PURE__ */ import_react2.default.createElement(NavbarMore, { + items: outOfRangeItemIndexes.map((i) => items[i]), + switchTo: props.switchCallback + }) : null)); + }; + var navBar_default = NavbarContent; + + // ../node_modules/spcr-navigation-bar/useNavigationBar.tsx + var useNavigationBar = (links) => { + const [activeLink, setActiveLink] = (0, import_react3.useState)(links[0]); + const navbar = /* @__PURE__ */ import_react3.default.createElement(navBar_default, { + links, + activeLink, + switchCallback: (link) => setActiveLink(link) + }); + return [navbar, activeLink, setActiveLink]; + }; + var useNavigationBar_default = useNavigationBar; + + // src/pages/top_artists.tsx + var import_react17 = __toESM(require_react()); + + // ../shared/dropdown/useDropdownMenu.tsx + var import_react5 = __toESM(require_react()); + + // ../shared/dropdown/dropdown.tsx + var import_react4 = __toESM(require_react()); + function CheckIcon() { + return /* @__PURE__ */ import_react4.default.createElement(Spicetify.ReactComponent.IconComponent, { + iconSize: 16, + semanticColor: "textBase", + dangerouslySetInnerHTML: { + __html: '<svg xmlns="http://www.w3.org/2000/svg"><path d="M15.53 2.47a.75.75 0 0 1 0 1.06L4.907 14.153.47 9.716a.75.75 0 0 1 1.06-1.06l3.377 3.376L14.47 2.47a.75.75 0 0 1 1.06 0z"/></svg>' + } + }); + } + var MenuItem = (props) => { + const { ReactComponent } = Spicetify; + const { option, isActive, switchCallback } = props; + const activeStyle = { + backgroundColor: "rgba(var(--spice-rgb-selected-row),.1)" + }; + return /* @__PURE__ */ import_react4.default.createElement(ReactComponent.MenuItem, { + trigger: "click", + onClick: () => switchCallback(option), + "data-checked": isActive, + trailingIcon: isActive ? /* @__PURE__ */ import_react4.default.createElement(CheckIcon, null) : void 0, + style: isActive ? activeStyle : void 0 + }, option.name); + }; + var DropdownMenu = (props) => { + const { ContextMenu, Menu, TextComponent } = Spicetify.ReactComponent; + const { options, activeOption, switchCallback } = props; + const optionItems = options.map((option) => { + return /* @__PURE__ */ import_react4.default.createElement(MenuItem, { + option, + isActive: option === activeOption, + switchCallback + }); + }); + const MenuWrapper2 = (props2) => { + return /* @__PURE__ */ import_react4.default.createElement(Menu, { + ...props2 + }, optionItems); + }; + return /* @__PURE__ */ import_react4.default.createElement(ContextMenu, { + menu: /* @__PURE__ */ import_react4.default.createElement(MenuWrapper2, null), + trigger: "click" + }, /* @__PURE__ */ import_react4.default.createElement("button", { + className: "x-sortBox-sortDropdown", + type: "button", + role: "combobox", + "aria-expanded": "false" + }, /* @__PURE__ */ import_react4.default.createElement(TextComponent, { + variant: "mesto", + semanticColor: "textSubdued" + }, activeOption.name), /* @__PURE__ */ import_react4.default.createElement("svg", { + role: "img", + height: "16", + width: "16", + "aria-hidden": "true", + className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small", + viewBox: "0 0 16 16", + "data-encore-id": "icon" + }, /* @__PURE__ */ import_react4.default.createElement("path", { + d: "m14 6-6 6-6-6h12z" + })))); + }; + var dropdown_default = DropdownMenu; + + // ../shared/dropdown/useDropdownMenu.tsx + var useDropdownMenu = (options, storageVariable) => { + const initialOptionID = storageVariable && Spicetify.LocalStorage.get(`${storageVariable}:active-option`); + const initialOption = initialOptionID && options.find((e) => e.id === initialOptionID); + const [activeOption, setActiveOption] = (0, import_react5.useState)(initialOption || options[0]); + const [availableOptions, setAvailableOptions] = (0, import_react5.useState)(options); + const dropdown = /* @__PURE__ */ import_react5.default.createElement(dropdown_default, { + options: availableOptions, + activeOption, + switchCallback: (option) => { + setActiveOption(option); + if (storageVariable) + Spicetify.LocalStorage.set(`${storageVariable}:active-option`, option.id); + } + }); + return [dropdown, activeOption, setActiveOption, setAvailableOptions]; + }; + var useDropdownMenu_default = useDropdownMenu; + + // ../library/src/components/collection_menu.tsx + var import_react8 = __toESM(require_react()); + + // ../library/src/components/text_input_dialog.tsx + var import_react6 = __toESM(require_react()); + var TextInputDialog = (props) => { + const { ButtonPrimary } = Spicetify.ReactComponent; + 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; + + // ../library/src/components/leading_icon.tsx + var import_react7 = __toESM(require_react()); + var LeadingIcon = ({ path }) => { + return /* @__PURE__ */ import_react7.default.createElement(Spicetify.ReactComponent.IconComponent, { + semanticColor: "textSubdued", + dangerouslySetInnerHTML: { + __html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">${path}</svg>` + }, + iconSize: 16 + }); + }; + var leading_icon_default = LeadingIcon; + + // ../library/src/components/collection_menu.tsx + var editIconPath = '<path d="M11.838.714a2.438 2.438 0 0 1 3.448 3.448l-9.841 9.841c-.358.358-.79.633-1.267.806l-3.173 1.146a.75.75 0 0 1-.96-.96l1.146-3.173c.173-.476.448-.909.806-1.267l9.84-9.84zm2.387 1.06a.938.938 0 0 0-1.327 0l-9.84 9.842a1.953 1.953 0 0 0-.456.716L2 14.002l1.669-.604a1.95 1.95 0 0 0 .716-.455l9.841-9.841a.938.938 0 0 0 0-1.327z"></path>'; + var deleteIconPath = '<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z"></path><path d="M12 8.75H4v-1.5h8v1.5z"></path>'; + var CollectionMenu = ({ id }) => { + const { Menu, MenuItem: MenuItem2 } = Spicetify.ReactComponent; + const deleteCollection = () => { + SpicetifyLibrary.CollectionWrapper.deleteCollection(id); + }; + const renameCollection = () => { + const name = SpicetifyLibrary.CollectionWrapper.getCollection(id).name; + const rename = (newName) => { + SpicetifyLibrary.CollectionWrapper.renameCollection(id, newName); + }; + Spicetify.PopupModal.display({ + title: "Rename Collection", + content: /* @__PURE__ */ import_react8.default.createElement(text_input_dialog_default, { + def: name, + onSave: rename + }) + }); + }; + const image = SpicetifyLibrary.CollectionWrapper.getCollection(id).imgUrl; + const setCollectionImage = () => { + const setImg = (imgUrl) => { + SpicetifyLibrary.CollectionWrapper.setCollectionImage(id, imgUrl); + }; + Spicetify.PopupModal.display({ + title: "Set Collection Image", + content: /* @__PURE__ */ import_react8.default.createElement(text_input_dialog_default, { + def: image, + placeholder: "Image URL", + onSave: setImg + }) + }); + }; + const removeImage = () => { + SpicetifyLibrary.CollectionWrapper.removeCollectionImage(id); + }; + return /* @__PURE__ */ import_react8.default.createElement(Menu, null, /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: editIconPath + }), + onClick: renameCollection + }, "Rename"), /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: deleteIconPath + }), + onClick: deleteCollection + }, "Delete"), /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: editIconPath + }), + onClick: setCollectionImage + }, "Set Collection Image"), image && /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { + path: deleteIconPath + }), + onClick: removeImage + }, "Remove Collection Image")); + }; + var collection_menu_default = CollectionMenu; + + // ../library/src/components/folder_menu.tsx + var import_react9 = __toESM(require_react()); + var editIconPath2 = '<path d="M11.838.714a2.438 2.438 0 0 1 3.448 3.448l-9.841 9.841c-.358.358-.79.633-1.267.806l-3.173 1.146a.75.75 0 0 1-.96-.96l1.146-3.173c.173-.476.448-.909.806-1.267l9.84-9.84zm2.387 1.06a.938.938 0 0 0-1.327 0l-9.84 9.842a1.953 1.953 0 0 0-.456.716L2 14.002l1.669-.604a1.95 1.95 0 0 0 .716-.455l9.841-9.841a.938.938 0 0 0 0-1.327z"></path>'; + var deleteIconPath2 = '<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z"></path><path d="M12 8.75H4v-1.5h8v1.5z"></path>'; + var FolderMenu = ({ uri }) => { + const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; + const image = SpicetifyLibrary.FolderImageWrapper.getFolderImage(uri); + const setImage = () => { + const setNewImage = (newUrl) => { + SpicetifyLibrary.FolderImageWrapper.setFolderImage({ uri, url: newUrl }); + }; + Spicetify.PopupModal.display({ + title: "Set Folder Image", + content: /* @__PURE__ */ import_react9.default.createElement(text_input_dialog_default, { + def: image, + onSave: setNewImage + }) + }); + }; + const removeImage = () => { + SpicetifyLibrary.FolderImageWrapper.removeFolderImage(uri); + }; + return /* @__PURE__ */ import_react9.default.createElement(Menu, null, /* @__PURE__ */ import_react9.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { + path: editIconPath2 + }), + onClick: setImage + }, "Set Folder Image"), image && /* @__PURE__ */ import_react9.default.createElement(MenuItem2, { + leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { + path: deleteIconPath2 + }), + onClick: removeImage + }, "Remove Folder Image")); + }; + var folder_menu_default = FolderMenu; + + // ../shared/components/spotify_card.tsx + var import_react11 = __toESM(require_react()); + + // ../shared/components/folder_fallback.tsx + var import_react10 = __toESM(require_react()); + var FolderSVG = (e) => { + return /* @__PURE__ */ import_react10.default.createElement(Spicetify.ReactComponent.IconComponent, { + semanticColor: "textSubdued", + viewBox: "0 0 24 24", + size: "xxlarge", + dangerouslySetInnerHTML: { + __html: '<path 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"/>' + }, + ...e + }); + }; + var folder_fallback_default = FolderSVG; + + // ../shared/components/spotify_card.tsx + function SpotifyCard(props) { + 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 Menu = () => { + switch (type) { + case "artist": + return /* @__PURE__ */ import_react11.default.createElement(ArtistMenu, { + uri + }); + case "album": + return /* @__PURE__ */ import_react11.default.createElement(AlbumMenu, { + uri, + artistUri, + canRemove: true + }); + case "playlist": + return /* @__PURE__ */ import_react11.default.createElement(PlaylistMenu, { + uri + }); + case "show": + return /* @__PURE__ */ import_react11.default.createElement(PodcastShowMenu, { + uri + }); + case "collection": + return /* @__PURE__ */ import_react11.default.createElement(collection_menu_default, { + id: uri + }); + case "folder": + return /* @__PURE__ */ import_react11.default.createElement(folder_menu_default, { + uri + }); + default: + return /* @__PURE__ */ import_react11.default.createElement(import_react11.default.Fragment, null); + } + }; + const lastfmProps = type === "lastfm" ? { onClick: () => window.open(uri, "_blank"), isPlayable: false, delegateNavigation: true } : {}; + const folderProps = type === "folder" ? { + delegateNavigation: true, + onClick: () => { + createHref({ pathname: `/library/folder/${uri}` }); + push({ pathname: `/library/folder/${uri}` }); + } + } : {}; + const collectionProps = type === "collection" ? { + delegateNavigation: true, + onClick: () => { + createHref({ pathname: `/library/collection/${uri}` }); + push({ pathname: `/library/collection/${uri}` }); + } + } : {}; + return /* @__PURE__ */ import_react11.default.createElement(ContextMenu, { + menu: Menu(), + trigger: "right-click" + }, /* @__PURE__ */ import_react11.default.createElement(Card, { + featureIdentifier: type, + headerText: header, + renderCardImage: () => /* @__PURE__ */ import_react11.default.createElement(CardImage, { + images: [ + { + height: 640, + url: imageUrl, + width: 640 + } + ], + isCircular: type === "artist", + FallbackComponent: folder_fallback_default + }), + renderSubHeaderContent: () => /* @__PURE__ */ import_react11.default.createElement(TextComponent, { + as: "div", + variant: "mesto", + semanticColor: "textSubdued", + children: subheader + }), + uri, + ...lastfmProps, + ...folderProps, + ...collectionProps + })); + } + var spotify_card_default = SpotifyCard; + + // src/endpoints.ts + var lfmperiods = { + short_term: "1month", + medium_term: "6month", + long_term: "overall" + }; + var LASTFM = { + toptracks: (user, key, range) => `http://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${key}&format=json&period=${lfmperiods[range]}`, + topalbums: (user, key, range) => `https://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=${user}&api_key=${key}&format=json&period=${lfmperiods[range]}`, + topartists: (user, key, range) => `https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${key}&format=json&period=${lfmperiods[range]}`, + charts: (key, type) => `http://ws.audioscrobbler.com/2.0/?method=chart.gettop${type}&api_key=${key}&format=json` + }; + var SPOTIFY = { + toptracks: (range) => `https://api.spotify.com/v1/me/top/tracks?limit=50&offset=0&time_range=${range}`, + topartists: (range) => `https://api.spotify.com/v1/me/top/artists?limit=50&offset=0&time_range=${range}`, + artists: (artists) => `https://api.spotify.com/v1/artists?ids=${filter(artists)}`, + rootlist: "sp://core-playlist/v1/rootlist", + playlist: (uri) => `sp://core-playlist/v1/playlist/${uri}`, + search: (track, artist) => `https://api.spotify.com/v1/search?q=track:${filter(track)}+artist:${filter(artist)}&type=track`, + searchartist: (artist) => `https://api.spotify.com/v1/search?q=artist:${filter(artist)}&type=artist`, + searchalbum: (album, artist) => `https://api.spotify.com/v1/search?q=${filter(album)}+artist:${filter(artist)}&type=album`, + audiofeatures: (ids) => `https://api.spotify.com/v1/audio-features?ids=${ids}`, + queryliked: (ids) => `https://api.spotify.com/v1/me/tracks/contains?ids=${ids}` + }; + var PLACEHOLDER = "https://raw.githubusercontent.com/harbassan/spicetify-apps/main/stats/src/styles/placeholder.png"; + + // src/funcs.ts + function filter(str) { + const normalizedStr = str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + return normalizedStr.replace(/[^a-zA-Z0-9\-._~:/?#[\]@!$&()*+,;= ]/g, "").replace(/ /g, "+"); + } + var updatePageCache = (i, callback, activeOption, lib = false) => { + let cacheInfo = Spicetify.LocalStorage.get("stats:cache-info"); + if (!cacheInfo) + return; + let cacheInfoArray = JSON.parse(cacheInfo); + if (!cacheInfoArray[i]) { + if (!lib) { + ["short_term", "medium_term", "long_term"].filter((option) => option !== activeOption).forEach((option) => callback(option, true, false)); + } + if (lib === "charts") { + ["artists", "tracks"].filter((option) => option !== activeOption).forEach((option) => callback(option, true, false)); + } + callback(activeOption, true); + cacheInfoArray[i] = true; + Spicetify.LocalStorage.set("stats:cache-info", JSON.stringify(cacheInfoArray)); + } + }; + var apiRequest = async (name, url, timeout = 5, log = true) => { + try { + const timeStart = window.performance.now(); + const response = await Spicetify.CosmosAsync.get(url); + if (log) + console.log("stats -", name, "fetch time:", window.performance.now() - timeStart); + return response; + } catch (e) { + if (timeout === 0) { + console.log("stats -", name, "all requests failed:", e); + console.log("stats -", name, "giving up"); + return null; + } else { + if (timeout === 5) { + console.log("stats -", name, "request failed:", e); + console.log("stats -", name, "retrying..."); + } + await new Promise((resolve) => setTimeout(resolve, 5e3)); + return apiRequest(name, url, timeout - 1); + } + } + }; + var fetchAudioFeatures = async (ids) => { + const batchSize = 100; + const batches = []; + ids = ids.filter((id) => id.match(/^[a-zA-Z0-9]{22}$/)); + for (let i = 0; i < ids.length; i += batchSize) { + const batch = ids.slice(i, i + batchSize); + batches.push(batch); + } + const promises = batches.map((batch, index) => { + return apiRequest(`audioFeaturesBatch${index}`, SPOTIFY.audiofeatures(batch.join(",")), 5, false); + }); + const responses = await Promise.all(promises); + const data = responses.reduce((acc, response) => { + if (!response?.audio_features) + return acc; + return acc.concat(response.audio_features); + }, []); + return data; + }; + var fetchTopAlbums = async (albums, cachedAlbums) => { + let album_keys = Object.keys(albums).filter((id) => id.match(/^[a-zA-Z0-9]{22}$/)).sort((a, b) => albums[b] - albums[a]).slice(0, 100); + let release_years = {}; + let total_album_tracks = 0; + const cachedAlbumsSet = new Set(cachedAlbums?.map((album) => album.uri)); + let top_albums = await Promise.all( + album_keys.map(async (albumID) => { + let albumMeta; + if (cachedAlbums && cachedAlbumsSet.has(`spotify:album:${albumID}`)) { + albumMeta = cachedAlbums.find((album) => album.uri === `spotify:album:${albumID}`); + } + if (!albumMeta) { + try { + albumMeta = await Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.getAlbum, { + uri: `spotify:album:${albumID}`, + locale: "en", + offset: 0, + limit: 50 + }); + if (!albumMeta?.data?.albumUnion?.name) + throw new Error("Invalid URI"); + } catch (e) { + console.error("stats - album metadata request failed:", e); + return; + } + } + const releaseYear = albumMeta?.release_year || albumMeta.data.albumUnion.date.isoString.slice(0, 4); + release_years[releaseYear] = (release_years[releaseYear] || 0) + albums[albumID]; + total_album_tracks += albums[albumID]; + return { + name: albumMeta.name || albumMeta.data.albumUnion.name, + uri: albumMeta.uri || albumMeta.data.albumUnion.uri, + image: albumMeta.image || albumMeta.data.albumUnion.coverArt.sources[0]?.url || PLACEHOLDER, + release_year: releaseYear, + freq: albums[albumID] + }; + }) + ); + top_albums = top_albums.filter((el) => el != null).slice(0, 10); + return [top_albums, Object.entries(release_years), total_album_tracks]; + }; + var fetchTopArtists = async (artists) => { + if (Object.keys(artists).length === 0) + return [[], [], 0]; + let artist_keys = Object.keys(artists).filter((id) => id.match(/^[a-zA-Z0-9]{22}$/)).sort((a, b) => artists[b] - artists[a]).slice(0, 50); + let genres = {}; + let total_genre_tracks = 0; + const artistsMeta = await apiRequest("artistsMetadata", SPOTIFY.artists(artist_keys.join(","))); + let top_artists = artistsMeta?.artists?.map((artist) => { + if (!artist) + return null; + artist.genres.forEach((genre) => { + genres[genre] = (genres[genre] || 0) + artists[artist.id]; + }); + total_genre_tracks += artists[artist.id]; + return { + name: artist.name, + uri: artist.uri, + image: artist.images[2]?.url || PLACEHOLDER, + freq: artists[artist.id] + }; + }); + top_artists = top_artists.filter((el) => el != null).slice(0, 10); + const top_genres = Object.entries(genres).sort((a, b) => b[1] - a[1]).slice(0, 10); + return [top_artists, top_genres, total_genre_tracks]; + }; + var convertTrackData = async (data) => { + return await Promise.all( + data.map(async (item) => { + const spotifyItem = await Spicetify.CosmosAsync.get(SPOTIFY.search(item.name, item.artist.name)).then( + (res) => res.tracks?.items[0] + ); + if (!spotifyItem) { + console.log(`couldn't find track: ${item.name} by ${item.artist.name}`); + return { + name: item.name, + image: PLACEHOLDER, + uri: item.url, + artists: [{ name: item.artist.name, uri: item.artist.url }], + duration: 0, + album: "N/A", + popularity: 0, + explicit: false, + album_uri: item.url + }; + } + return { + name: item.name, + image: spotifyItem.album.images[0].url, + uri: spotifyItem.uri, + id: spotifyItem.id, + artists: spotifyItem.artists.map((artist) => ({ name: artist.name, uri: artist.uri })), + duration: spotifyItem.duration_ms, + album: spotifyItem.album.name, + popularity: spotifyItem.popularity, + explicit: spotifyItem.explicit, + album_uri: spotifyItem.album.uri, + release_year: spotifyItem.album.release_date.slice(0, 4) + }; + }) + ); + }; + var convertAlbumData = async (data) => { + return await Promise.all( + data.map(async (item) => { + const spotifyItem = await Spicetify.CosmosAsync.get(SPOTIFY.searchalbum(item.name, item.artist.name)).then( + (res) => res.albums?.items[0] + ); + if (!spotifyItem) { + console.log(`couldn't find album: ${item.name} by ${item.artist.name}`); + return { + name: item.name, + image: PLACEHOLDER, + uri: item.url, + id: item.mbid + }; + } + return { + name: item.name, + image: spotifyItem.images[0].url, + uri: spotifyItem.uri, + id: spotifyItem.id + }; + }) + ); + }; + var convertArtistData = async (data) => { + return await Promise.all( + data.map(async (item) => { + const spotifyItem = await Spicetify.CosmosAsync.get(SPOTIFY.searchartist(item.name)).then( + (res) => res.artists?.items[0] + ); + if (!spotifyItem) { + console.log(`couldn't find artist: ${item.name}`); + return { + name: item.name, + image: PLACEHOLDER, + uri: item.url, + id: item.mbid + }; + } + return { + name: item.name, + image: spotifyItem.images[0].url, + uri: spotifyItem.uri, + id: spotifyItem.id, + genres: spotifyItem.genres + }; + }) + ); + }; + var checkLiked = async (tracks) => { + const nullIndexes = []; + tracks.forEach((track, index) => { + if (track === null) { + nullIndexes.push(index); + } + }); + const apiResponse = await apiRequest("checkLiked", SPOTIFY.queryliked(tracks.filter((e) => e).join(","))); + if (!apiResponse) + return; + const response = []; + let nullIndexesIndex = 0; + for (let i = 0; i < tracks.length; i++) { + if (nullIndexes.includes(i)) { + response.push(false); + } else { + response.push(apiResponse[nullIndexesIndex]); + nullIndexesIndex++; + } + } + return response; + }; + async function queue(list, context = null) { + list.push("spotify:delimiter"); + const { _queue, _client } = Spicetify.Platform.PlayerAPI._queue; + const { prevTracks, queueRevision } = _queue; + const nextTracks = list.map((uri) => ({ + contextTrack: { + uri, + uid: "", + metadata: { + is_queued: "false" + } + }, + removed: [], + blocked: [], + provider: "context" + })); + _client.setQueue({ + nextTracks, + prevTracks, + queueRevision + }); + if (context) { + const { sessionId } = Spicetify.Platform.PlayerAPI.getState(); + Spicetify.Platform.PlayerAPI.updateContext(sessionId, { + uri: `spotify:user:${Spicetify.Platform.LibraryAPI._currentUsername}:top:tracks`, + url: "" + }); + } + Spicetify.Player.next(); + } + + // ../shared/components/status.tsx + var import_react12 = __toESM(require_react()); + var ErrorIcon = () => { + return /* @__PURE__ */ import_react12.default.createElement("svg", { + "data-encore-id": "icon", + role: "img", + "aria-hidden": "true", + viewBox: "0 0 24 24", + className: "status-icon" + }, /* @__PURE__ */ import_react12.default.createElement("path", { + d: "M11 18v-2h2v2h-2zm0-4V6h2v8h-2z" + }), /* @__PURE__ */ import_react12.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_react12.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_react12.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_react12.default.useState(false); + import_react12.default.useEffect(() => { + const to = setTimeout(() => { + setIsVisible(true); + }, 500); + return () => clearTimeout(to); + }, []); + return isVisible ? /* @__PURE__ */ import_react12.default.createElement(import_react12.default.Fragment, null, /* @__PURE__ */ import_react12.default.createElement("div", { + className: "loadingWrapper" + }, props.icon === "error" ? /* @__PURE__ */ import_react12.default.createElement(ErrorIcon, null) : /* @__PURE__ */ import_react12.default.createElement(LibraryIcon, null), /* @__PURE__ */ import_react12.default.createElement("h1", null, props.heading), /* @__PURE__ */ import_react12.default.createElement("h3", null, props.subheading))) : /* @__PURE__ */ import_react12.default.createElement(import_react12.default.Fragment, null); + }; + var status_default = Status; + + // ../shared/components/page_container.tsx + var import_react14 = __toESM(require_react()); + + // src/components/buttons/create_playlist_button.tsx + var import_react13 = __toESM(require_react()); + async function createPlaylistAsync(infoToCreatePlaylist) { + const { Platform, showNotification } = Spicetify; + const { RootlistAPI, PlaylistAPI } = Platform; + try { + const { playlistName, itemsUris } = infoToCreatePlaylist; + const playlistUri = await RootlistAPI.createPlaylist(playlistName, { before: "start" }); + await PlaylistAPI.add(playlistUri, itemsUris, { before: "start" }); + } catch (error) { + console.error(error); + showNotification("Failed to create playlist", true, 1e3); + } + } + function CreatePlaylistButton(props) { + const { TooltipWrapper, ButtonSecondary } = Spicetify.ReactComponent; + const { infoToCreatePlaylist } = props; + return /* @__PURE__ */ import_react13.default.createElement(TooltipWrapper, { + label: "Turn Into Playlist", + renderInline: true, + placement: "top" + }, /* @__PURE__ */ import_react13.default.createElement(ButtonSecondary, { + "aria-label": "Turn Into Playlist", + children: "Turn Into Playlist", + semanticColor: "textBase", + buttonSize: "sm", + onClick: () => createPlaylistAsync(infoToCreatePlaylist), + className: "stats-make-playlist-button" + })); + } + var create_playlist_button_default = CreatePlaylistButton; + + // ../shared/components/page_container.tsx + var PageContainer = (props) => { + const { title, infoToCreatePlaylist, headerEls, children } = props; + const { TextComponent } = Spicetify.ReactComponent; + return /* @__PURE__ */ import_react14.default.createElement("section", { + className: "contentSpacing" + }, /* @__PURE__ */ import_react14.default.createElement("div", { + className: "page-header" + }, /* @__PURE__ */ import_react14.default.createElement("div", { + className: "header-left" + }, /* @__PURE__ */ import_react14.default.createElement(TextComponent, { + children: title, + as: "h1", + variant: "canon", + semanticColor: "textBase" + }), infoToCreatePlaylist ? /* @__PURE__ */ import_react14.default.createElement(create_playlist_button_default, { + infoToCreatePlaylist + }) : null), /* @__PURE__ */ import_react14.default.createElement("div", { + className: "header-right" + }, headerEls)), /* @__PURE__ */ import_react14.default.createElement("div", { + className: "page-content" + }, children)); + }; + var page_container_default = PageContainer; + + // ../shared/components/settings_button.tsx + var import_react15 = __toESM(require_react()); + function SettingsIcon() { + return /* @__PURE__ */ import_react15.default.createElement(Spicetify.ReactComponent.IconComponent, { + semanticColor: "textSubdued", + dangerouslySetInnerHTML: { + __html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/></svg>' + }, + iconSize: 16 + }); + } + function SettingsButton(props) { + const { TooltipWrapper, ButtonTertiary } = Spicetify.ReactComponent; + const { configWrapper } = props; + return /* @__PURE__ */ import_react15.default.createElement(TooltipWrapper, { + label: "Settings", + renderInline: true, + placement: "top" + }, /* @__PURE__ */ import_react15.default.createElement(ButtonTertiary, { + buttonSize: "sm", + onClick: configWrapper.launchModal, + "aria-label": "Settings", + iconOnly: SettingsIcon + })); + } + var settings_button_default = SettingsButton; + + // src/components/buttons/refresh_button.tsx + var import_react16 = __toESM(require_react()); + function RefreshIcon() { + return /* @__PURE__ */ import_react16.default.createElement(Spicetify.ReactComponent.IconComponent, { + semanticColor: "textSubdued", + iconSize: 16, + dangerouslySetInnerHTML: { + __html: '<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 4.75A3.75 3.75 0 0 1 3.75 1h8.5A3.75 3.75 0 0 1 16 4.75v5a3.75 3.75 0 0 1-3.75 3.75H9.81l1.018 1.018a.75.75 0 1 1-1.06 1.06L6.939 12.75l2.829-2.828a.75.75 0 1 1 1.06 1.06L9.811 12h2.439a2.25 2.25 0 0 0 2.25-2.25v-5a2.25 2.25 0 0 0-2.25-2.25h-8.5A2.25 2.25 0 0 0 1.5 4.75v5A2.25 2.25 0 0 0 3.75 12H5v1.5H3.75A3.75 3.75 0 0 1 0 9.75v-5z"/></svg>' + } + }); + } + function RefreshButton(props) { + const { ButtonTertiary, TooltipWrapper } = Spicetify.ReactComponent; + const { callback } = props; + return /* @__PURE__ */ import_react16.default.createElement(TooltipWrapper, { + label: "Refresh", + renderInline: true, + placement: "top" + }, /* @__PURE__ */ import_react16.default.createElement(ButtonTertiary, { + buttonSize: "sm", + onClick: callback, + "aria-label": "Refresh", + iconOnly: RefreshIcon + })); + } + var refresh_button_default = RefreshButton; + + // src/pages/top_artists.tsx + var topArtistsReq = async (time_range, configWrapper) => { + const { config } = configWrapper; + if (config["use-lastfm"] === true) { + if (!config["api-key"] || !config["lastfm-user"]) + return 300; + const { ["lastfm-user"]: user, ["api-key"]: key } = config; + const response = await apiRequest("lastfm", LASTFM.topartists(user, key, time_range)); + if (!response) + return 200; + return await convertArtistData(response.topartists.artist); + } else { + const response = await apiRequest("topArtists", SPOTIFY.topartists(time_range)); + if (!response) + return 200; + return response.items.map((artist) => { + const image = artist.images[2]?.url || artist.images[1]?.url || PLACEHOLDER; + return { + id: artist.id, + name: artist.name, + image, + uri: artist.uri, + genres: artist.genres + }; + }); + } + }; + var DropdownOptions = [ + { id: "short_term", name: "Past Month" }, + { id: "medium_term", name: "Past 6 Months" }, + { id: "long_term", name: "All Time" } + ]; + var ArtistsPage = ({ configWrapper }) => { + const [topArtists, setTopArtists] = import_react17.default.useState(100); + const [dropdown, activeOption, setActiveOption] = useDropdownMenu_default(DropdownOptions, "stats:top-artists"); + const fetchTopArtists2 = async (time_range, force, set = true) => { + if (!force) { + let storedData = Spicetify.LocalStorage.get(`stats:top-artists:${time_range}`); + if (storedData) + return setTopArtists(JSON.parse(storedData)); + } + const start = window.performance.now(); + const topArtists2 = await topArtistsReq(time_range, configWrapper); + if (set) + setTopArtists(topArtists2); + Spicetify.LocalStorage.set(`stats:top-artists:${time_range}`, JSON.stringify(topArtists2)); + console.log("total artists fetch time:", window.performance.now() - start); + }; + import_react17.default.useEffect(() => { + updatePageCache(0, fetchTopArtists2, activeOption.id); + }, []); + import_react17.default.useEffect(() => { + fetchTopArtists2(activeOption.id); + }, [activeOption]); + const refresh = () => { + fetchTopArtists2(activeOption.id, true); + }; + const props = { + title: "Top Artists", + headerEls: [dropdown, /* @__PURE__ */ import_react17.default.createElement(refresh_button_default, { + callback: refresh + }), /* @__PURE__ */ import_react17.default.createElement(settings_button_default, { + configWrapper + })] + }; + switch (topArtists) { + case 300: + return /* @__PURE__ */ import_react17.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react17.default.createElement(status_default, { + icon: "error", + heading: "No API Key or Username", + subheading: "Please enter these in the settings menu" + })); + case 200: + return /* @__PURE__ */ import_react17.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react17.default.createElement(status_default, { + icon: "error", + heading: "Failed to Fetch Top Artists", + subheading: "An error occurred while fetching the data" + })); + case 100: + return /* @__PURE__ */ import_react17.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react17.default.createElement(status_default, { + icon: "library", + heading: "Loading", + subheading: "Fetching data..." + })); + } + const artistCards = topArtists.map((artist, index) => /* @__PURE__ */ import_react17.default.createElement(spotify_card_default, { + type: artist.uri.includes("last") ? "lastfm" : "artist", + uri: artist.uri, + header: artist.name, + subheader: `#${index + 1} Artist`, + imageUrl: artist.image + })); + return /* @__PURE__ */ import_react17.default.createElement(import_react17.default.Fragment, null, /* @__PURE__ */ import_react17.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react17.default.createElement("div", { + className: `main-gridContainer-gridContainer grid` + }, artistCards))); + }; + var top_artists_default = import_react17.default.memo(ArtistsPage); + + // src/pages/top_tracks.tsx + var import_react20 = __toESM(require_react()); + + // src/components/track_row.tsx + var import_react18 = __toESM(require_react()); + var ArtistLink = ({ name, uri, index, length }) => { + return /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement("a", { + draggable: "true", + dir: "auto", + href: uri, + tabIndex: -1 + }, name), index === length ? null : ", "); + }; + var ExplicitBadge = import_react18.default.memo(() => { + return /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement("span", { + className: "TypeElement-ballad-textSubdued-type main-trackList-rowBadges", + "data-encore-id": "type" + }, /* @__PURE__ */ import_react18.default.createElement("span", { + "aria-label": "Explicit", + className: "main-tag-container", + title: "Explicit" + }, "E"))); + }); + var LikedIcon = ({ active, uri }) => { + const [liked, setLiked] = import_react18.default.useState(active); + const toggleLike = () => { + if (liked) { + Spicetify.Platform.LibraryAPI.remove(uri); + } else { + Spicetify.Platform.LibraryAPI.add(uri); + } + setLiked(!liked); + }; + import_react18.default.useEffect(() => { + setLiked(active); + }, [active]); + return /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { + label: liked ? `Remove from Your Library` : "Save to Your Library", + placement: "top" + }, /* @__PURE__ */ import_react18.default.createElement("button", { + type: "button", + role: "switch", + "aria-checked": liked, + "aria-label": "Remove from Your Library", + onClick: toggleLike, + className: liked ? "main-addButton-button main-trackList-rowHeartButton main-addButton-active" : "main-addButton-button main-trackList-rowHeartButton", + tabIndex: -1 + }, /* @__PURE__ */ import_react18.default.createElement("svg", { + role: "img", + height: "16", + width: "16", + "aria-hidden": "true", + viewBox: "0 0 16 16", + "data-encore-id": "icon", + className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small" + }, /* @__PURE__ */ import_react18.default.createElement("path", { + d: liked ? "M15.724 4.22A4.313 4.313 0 0 0 12.192.814a4.269 4.269 0 0 0-3.622 1.13.837.837 0 0 1-1.14 0 4.272 4.272 0 0 0-6.21 5.855l5.916 7.05a1.128 1.128 0 0 0 1.727 0l5.916-7.05a4.228 4.228 0 0 0 .945-3.577z" : "M1.69 2A4.582 4.582 0 0 1 8 2.023 4.583 4.583 0 0 1 11.88.817h.002a4.618 4.618 0 0 1 3.782 3.65v.003a4.543 4.543 0 0 1-1.011 3.84L9.35 14.629a1.765 1.765 0 0 1-2.093.464 1.762 1.762 0 0 1-.605-.463L1.348 8.309A4.582 4.582 0 0 1 1.689 2zm3.158.252A3.082 3.082 0 0 0 2.49 7.337l.005.005L7.8 13.664a.264.264 0 0 0 .311.069.262.262 0 0 0 .09-.069l5.312-6.33a3.043 3.043 0 0 0 .68-2.573 3.118 3.118 0 0 0-2.551-2.463 3.079 3.079 0 0 0-2.612.816l-.007.007a1.501 1.501 0 0 1-2.045 0l-.009-.008a3.082 3.082 0 0 0-2.121-.861z" + })))); + }; + var DraggableComponent = ({ + uri, + title, + ...props + }) => { + const dragHandler = Spicetify.ReactHook.DragHandler?.([uri], title); + return /* @__PURE__ */ import_react18.default.createElement("div", { + onDragStart: dragHandler, + draggable: "true", + ...props + }, props.children); + }; + function playAndQueue(uri, uris) { + uris = uris.filter((u) => !u.includes("last")); + uris = uris.concat(uris.splice(0, uris.indexOf(uri))); + queue(uris); + } + var MenuWrapper = import_react18.default.memo((props) => /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.AlbumMenu, { + ...props + })); + var TrackRow = (props) => { + const ArtistLinks = props.artists.map((artist, index) => { + return /* @__PURE__ */ import_react18.default.createElement(ArtistLink, { + index, + length: props.artists.length - 1, + name: artist.name, + uri: artist.uri + }); + }); + return /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.ContextMenu, { + menu: /* @__PURE__ */ import_react18.default.createElement(MenuWrapper, { + uri: props.uri + }), + trigger: "right-click" + }, /* @__PURE__ */ import_react18.default.createElement("div", { + role: "row", + "aria-rowindex": 2, + "aria-selected": "false" + }, /* @__PURE__ */ import_react18.default.createElement(DraggableComponent, { + uri: props.uri, + title: `${props.name} \u2022 ${props.artists.map((artist) => artist.name).join(", ")}`, + className: "main-trackList-trackListRow main-trackList-trackListRowGrid", + role: "presentation", + onClick: (event) => event.detail === 2 && playAndQueue(props.uri, props.uris), + style: { height: 56 } + }, /* @__PURE__ */ import_react18.default.createElement("div", { + className: "main-trackList-rowSectionIndex", + role: "gridcell", + "aria-colindex": 1, + tabIndex: -1 + }, /* @__PURE__ */ import_react18.default.createElement("div", { + uri: props.uri, + className: "main-trackList-rowMarker" + }, /* @__PURE__ */ import_react18.default.createElement("span", { + className: "TypeElement-ballad-type main-trackList-number", + "data-encore-id": "type" + }, props.index), /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { + label: `Play ${props.name} by ${props.artists.map((artist) => artist.name).join(", ")}`, + placement: "top" + }, /* @__PURE__ */ import_react18.default.createElement("button", { + className: "main-trackList-rowImagePlayButton", + "aria-label": `Play ${props.name}`, + tabIndex: -1, + onClick: () => playAndQueue(props.uri, props.uris) + }, /* @__PURE__ */ import_react18.default.createElement("svg", { + role: "img", + height: "24", + width: "24", + "aria-hidden": "true", + className: "Svg-img-24 Svg-img-24-icon main-trackList-rowPlayPauseIcon", + viewBox: "0 0 24 24", + "data-encore-id": "icon" + }, /* @__PURE__ */ import_react18.default.createElement("path", { + d: "m7.05 3.606 13.49 7.788a.7.7 0 0 1 0 1.212L7.05 20.394A.7.7 0 0 1 6 19.788V4.212a.7.7 0 0 1 1.05-.606z" + })))))), /* @__PURE__ */ import_react18.default.createElement("div", { + className: "main-trackList-rowSectionStart", + role: "gridcell", + "aria-colindex": 2, + tabIndex: -1 + }, /* @__PURE__ */ import_react18.default.createElement("img", { + "aria-hidden": "false", + draggable: "false", + loading: "eager", + src: props.image, + alt: "", + className: "main-image-image main-trackList-rowImage main-image-loaded", + width: "40", + height: "40" + }), /* @__PURE__ */ import_react18.default.createElement("div", { + className: "main-trackList-rowMainContent" + }, /* @__PURE__ */ import_react18.default.createElement("div", { + dir: "auto", + className: "TypeElement-ballad-textBase TypeElement-ballad-textBase-type main-trackList-rowTitle standalone-ellipsis-one-line", + "data-encore-id": "type" + }, props.name), props.explicit && /* @__PURE__ */ import_react18.default.createElement(ExplicitBadge, null), /* @__PURE__ */ import_react18.default.createElement("span", { + className: "TypeElement-mesto-textSubdued TypeElement-mesto-textSubdued-type main-trackList-rowSubTitle standalone-ellipsis-one-line", + "data-encore-id": "type" + }, ArtistLinks))), /* @__PURE__ */ import_react18.default.createElement("div", { + className: "main-trackList-rowSectionVariable", + role: "gridcell", + "aria-colindex": 3, + tabIndex: -1 + }, /* @__PURE__ */ import_react18.default.createElement("span", { + "data-encore-id": "type", + className: "TypeElement-mesto TypeElement-mesto-type" + }, /* @__PURE__ */ import_react18.default.createElement("a", { + draggable: "true", + className: "standalone-ellipsis-one-line", + dir: "auto", + href: props.album_uri, + tabIndex: -1 + }, props.album))), /* @__PURE__ */ import_react18.default.createElement("div", { + className: "main-trackList-rowSectionEnd", + role: "gridcell", + "aria-colindex": 5, + tabIndex: -1 + }, /* @__PURE__ */ import_react18.default.createElement(LikedIcon, { + active: props.liked || false, + uri: props.uri + }), /* @__PURE__ */ import_react18.default.createElement("div", { + className: "TypeElement-mesto-textSubdued TypeElement-mesto-textSubdued-type main-trackList-rowDuration", + "data-encore-id": "type" + }, Spicetify.Player.formatTime(props.duration)), /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.ContextMenu, { + menu: /* @__PURE__ */ import_react18.default.createElement(MenuWrapper, { + uri: props.uri + }), + trigger: "click" + }, /* @__PURE__ */ import_react18.default.createElement("button", { + type: "button", + "aria-haspopup": "menu", + "aria-label": `More options for ${props.name}`, + className: "main-moreButton-button Button-sm-16-buttonTertiary-iconOnly-condensed-useBrowserDefaultFocusStyle Button-small-small-buttonTertiary-iconOnly-condensed-useBrowserDefaultFocusStyle main-trackList-rowMoreButton", + tabIndex: -1 + }, /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { + label: `More options for ${props.name} by ${props.artists.map((artist) => artist.name).join(", ")}`, + placement: "top" + }, /* @__PURE__ */ import_react18.default.createElement("span", null, /* @__PURE__ */ import_react18.default.createElement("svg", { + role: "img", + height: "16", + width: "16", + "aria-hidden": "true", + viewBox: "0 0 16 16", + "data-encore-id": "icon", + className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small" + }, /* @__PURE__ */ import_react18.default.createElement("path", { + d: "M3 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm6.5 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zM16 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z" + }))))))))))); + }; + var track_row_default = import_react18.default.memo(TrackRow); + + // src/components/tracklist.tsx + var import_react19 = __toESM(require_react()); + var Tracklist = ({ minified = false, children }) => { + return /* @__PURE__ */ import_react19.default.createElement("div", { + role: "grid", + "aria-rowcount": minified ? 5 : 50, + "aria-colcount": 4, + className: "main-trackList-trackList main-trackList-indexable", + tabIndex: 0 + }, !minified && /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-trackList-trackListHeader", + role: "presentation" + }, /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-trackList-trackListHeaderRow main-trackList-trackListRowGrid", + role: "row", + "aria-rowindex": 1 + }, /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-trackList-rowSectionIndex", + role: "columnheader", + "aria-colindex": 1, + "aria-sort": "none", + tabIndex: -1 + }, "#"), /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-trackList-rowSectionStart", + role: "columnheader", + "aria-colindex": 2, + "aria-sort": "none", + tabIndex: -1 + }, /* @__PURE__ */ import_react19.default.createElement("button", { + className: "main-trackList-column main-trackList-sortable", + tabIndex: -1 + }, /* @__PURE__ */ import_react19.default.createElement("span", { + className: "TypeElement-mesto-type standalone-ellipsis-one-line", + "data-encore-id": "type" + }, "Title"))), /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-trackList-rowSectionVariable", + role: "columnheader", + "aria-colindex": 3, + "aria-sort": "none", + tabIndex: -1 + }, /* @__PURE__ */ import_react19.default.createElement("button", { + className: "main-trackList-column main-trackList-sortable", + tabIndex: -1 + }, /* @__PURE__ */ import_react19.default.createElement("span", { + className: "TypeElement-mesto-type standalone-ellipsis-one-line", + "data-encore-id": "type" + }, "Album"))), /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-trackList-rowSectionEnd", + role: "columnheader", + "aria-colindex": 5, + "aria-sort": "none", + tabIndex: -1 + }, /* @__PURE__ */ import_react19.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { + label: "Duration", + placement: "top" + }, /* @__PURE__ */ import_react19.default.createElement("button", { + "aria-label": "Duration", + className: "main-trackList-column main-trackList-durationHeader main-trackList-sortable", + tabIndex: -1 + }, /* @__PURE__ */ import_react19.default.createElement("svg", { + role: "img", + height: "16", + width: "16", + "aria-hidden": "true", + viewBox: "0 0 16 16", + "data-encore-id": "icon", + className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small" + }, /* @__PURE__ */ import_react19.default.createElement("path", { + d: "M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z" + }), /* @__PURE__ */ import_react19.default.createElement("path", { + d: "M8 3.25a.75.75 0 0 1 .75.75v3.25H11a.75.75 0 0 1 0 1.5H7.25V4A.75.75 0 0 1 8 3.25z" + }))))))), /* @__PURE__ */ import_react19.default.createElement("div", { + className: "main-rootlist-wrapper", + role: "presentation", + style: { height: (minified ? 5 : 50) * 56 } + }, /* @__PURE__ */ import_react19.default.createElement("div", { + role: "presentation" + }, children))); + }; + var tracklist_default = Tracklist; + + // src/pages/top_tracks.tsx + var topTracksReq = async (time_range, configWrapper) => { + const { config } = configWrapper; + if (config["use-lastfm"] === true) { + if (!config["api-key"] || !config["lastfm-user"]) + return 300; + const { ["lastfm-user"]: user, ["api-key"]: key } = config; + const lastfmData = await apiRequest("lastfm", LASTFM.toptracks(user, key, time_range)); + if (!lastfmData) + return 200; + const spotifyData = await convertTrackData(lastfmData.toptracks.track); + const likedArray2 = await checkLiked(spotifyData.map((track) => track.id)); + if (!likedArray2) + return 200; + spotifyData.forEach((track, index) => { + track.liked = likedArray2[index]; + }); + return spotifyData; + } + const response = await apiRequest("topTracks", SPOTIFY.toptracks(time_range)); + if (!response) + return 200; + const likedArray = await checkLiked(response.items.map((track) => track.id)); + if (!likedArray) + return 200; + return response.items.map((track, index) => { + const images = track.album.images; + const image = images[2]?.url || images[1]?.url || PLACEHOLDER; + return { + liked: likedArray[index], + name: track.name, + image, + uri: track.uri, + id: track.id, + artists: track.artists.map((artist) => ({ + name: artist.name, + uri: artist.uri + })), + duration: track.duration_ms, + album: track.album.name, + album_uri: track.album.uri, + popularity: track.popularity, + explicit: track.explicit, + release_year: track.album.release_date.slice(0, 4) + }; + }); + }; + var DropdownOptions2 = [ + { id: "short_term", name: "Past Month" }, + { id: "medium_term", name: "Past 6 Months" }, + { id: "long_term", name: "All Time" } + ]; + var TracksPage = ({ configWrapper }) => { + const { LocalStorage } = Spicetify; + const [topTracks, setTopTracks] = import_react20.default.useState(100); + const [dropdown, activeOption] = useDropdownMenu_default(DropdownOptions2, "stats:top-tracks"); + const fetchTopTracks = async (time_range, force, set = true) => { + if (!force) { + const storedData = LocalStorage.get(`stats:top-tracks:${time_range}`); + if (storedData) + return setTopTracks(JSON.parse(storedData)); + } + const start = window.performance.now(); + const topTracks2 = await topTracksReq(time_range, configWrapper); + if (set) + setTopTracks(topTracks2); + LocalStorage.set(`stats:top-tracks:${time_range}`, JSON.stringify(topTracks2)); + console.log("total tracks fetch time:", window.performance.now() - start); + }; + import_react20.default.useEffect(() => { + updatePageCache(1, fetchTopTracks, activeOption.id); + }, []); + import_react20.default.useEffect(() => { + fetchTopTracks(activeOption.id); + }, [activeOption]); + const refresh = () => { + fetchTopTracks(activeOption.id, true); + }; + const props = { + title: "Top Tracks", + headerEls: [dropdown, /* @__PURE__ */ import_react20.default.createElement(refresh_button_default, { + callback: refresh + }), /* @__PURE__ */ import_react20.default.createElement(settings_button_default, { + configWrapper + })] + }; + switch (topTracks) { + case 300: + return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react20.default.createElement(status_default, { + icon: "error", + heading: "No API Key or Username", + subheading: "Please enter these in the settings menu" + })); + case 200: + return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react20.default.createElement(status_default, { + icon: "error", + heading: "Failed to Fetch Top Tracks", + subheading: "An error occurred while fetching the data" + })); + case 100: + return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react20.default.createElement(status_default, { + icon: "library", + heading: "Loading", + subheading: "Fetching data..." + })); + } + const infoToCreatePlaylist = { + playlistName: `Top Songs - ${activeOption}`, + itemsUris: topTracks.map((track) => track.uri) + }; + const trackRows = topTracks.map((track, index) => /* @__PURE__ */ import_react20.default.createElement(track_row_default, { + index: index + 1, + ...track, + uris: topTracks.map((track2) => track2.uri) + })); + return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { + ...props, + infoToCreatePlaylist + }, /* @__PURE__ */ import_react20.default.createElement(tracklist_default, null, trackRows)); + }; + var top_tracks_default = import_react20.default.memo(TracksPage); + + // src/pages/top_genres.tsx + var import_react25 = __toESM(require_react()); + + // src/components/cards/stat_card.tsx + var import_react21 = __toESM(require_react()); + function formatValue(name, value) { + switch (name) { + case "tempo": + return `${Math.round(value)} bpm`; + case "popularity": + return `${Math.round(value)} %`; + default: + return `${Math.round(value * 100)} %`; + } + } + function normalizeString(inputString) { + return inputString.charAt(0).toUpperCase() + inputString.slice(1).toLowerCase(); + } + function StatCard(props) { + const { TextComponent } = Spicetify.ReactComponent; + const { label, value } = props; + return /* @__PURE__ */ import_react21.default.createElement("div", { + className: "main-card-card" + }, /* @__PURE__ */ import_react21.default.createElement(TextComponent, { + as: "div", + semanticColor: "textBase", + variant: "alto", + children: typeof value === "number" ? formatValue(label, value) : value + }), /* @__PURE__ */ import_react21.default.createElement(TextComponent, { + as: "div", + semanticColor: "textBase", + variant: "balladBold", + children: normalizeString(label) + })); + } + var stat_card_default = StatCard; + + // src/components/cards/genres_card.tsx + var import_react22 = __toESM(require_react()); + var genreLine = (name, value, limit, total) => { + return /* @__PURE__ */ import_react22.default.createElement("div", { + className: "stats-genreRow" + }, /* @__PURE__ */ import_react22.default.createElement("div", { + className: "stats-genreRowFill", + style: { + width: `calc(${value / limit * 100}% + ${(limit - value) / (limit - 1) * 100}px)` + } + }, /* @__PURE__ */ import_react22.default.createElement("span", { + className: "stats-genreText" + }, name)), /* @__PURE__ */ import_react22.default.createElement("span", { + className: "stats-genreValue" + }, Math.round(value / total * 100) + "%")); + }; + var genreLines = (genres, total) => { + return genres.map(([genre, value]) => { + return genreLine(genre, value, genres[0][1], total); + }); + }; + var genresCard = ({ genres, total }) => { + const genresArray = genres.sort(([, a], [, b]) => b - a).slice(0, 10); + return /* @__PURE__ */ import_react22.default.createElement("div", { + className: `main-card-card stats-genreCard` + }, genreLines(genresArray, total)); + }; + var genres_card_default = genresCard; + + // src/components/inline_grid.tsx + var import_react23 = __toESM(require_react()); + function scrollGrid(event) { + const { target } = event; + if (!(target instanceof HTMLElement)) + return; + const grid = target.parentNode?.querySelector("div"); + if (!grid) + return; + grid.scrollLeft += grid.clientWidth; + if (grid.scrollWidth - grid.clientWidth - grid.scrollLeft <= grid.clientWidth) { + grid.setAttribute("data-scroll", "end"); + } else { + grid.setAttribute("data-scroll", "both"); + } + } + function scrollGridLeft(event) { + const { target } = event; + if (!(target instanceof HTMLElement)) + return; + const grid = target.parentNode?.querySelector("div"); + if (!grid) + return; + grid.scrollLeft -= grid.clientWidth; + if (grid.scrollLeft <= grid.clientWidth) { + grid.setAttribute("data-scroll", "start"); + } else { + grid.setAttribute("data-scroll", "both"); + } + } + function InlineGrid(props) { + const { children, special } = props; + return /* @__PURE__ */ import_react23.default.createElement("section", { + className: "stats-gridInlineSection" + }, /* @__PURE__ */ import_react23.default.createElement("button", { + className: "stats-scrollButton", + onClick: scrollGridLeft + }, "<"), /* @__PURE__ */ import_react23.default.createElement("button", { + className: "stats-scrollButton", + onClick: scrollGrid + }, ">"), /* @__PURE__ */ import_react23.default.createElement("div", { + className: `main-gridContainer-gridContainer stats-gridInline${special ? " stats-specialGrid" : ""}`, + "data-scroll": "start" + }, children)); + } + var inline_grid_default = import_react23.default.memo(InlineGrid); + + // src/components/shelf.tsx + var import_react24 = __toESM(require_react()); + function Shelf(props) { + const { TextComponent } = Spicetify.ReactComponent; + const { title, children } = props; + return /* @__PURE__ */ import_react24.default.createElement("section", { + className: "main-shelf-shelf Shelf" + }, /* @__PURE__ */ import_react24.default.createElement("div", { + className: "main-shelf-header" + }, /* @__PURE__ */ import_react24.default.createElement("div", { + className: "main-shelf-topRow" + }, /* @__PURE__ */ import_react24.default.createElement("div", { + className: "main-shelf-titleWrapper" + }, /* @__PURE__ */ import_react24.default.createElement(TextComponent, { + children: title, + as: "h2", + variant: "canon", + semanticColor: "textBase" + })))), /* @__PURE__ */ import_react24.default.createElement("section", null, children)); + } + var shelf_default = import_react24.default.memo(Shelf); + + // src/pages/top_genres.tsx + var DropdownOptions3 = [ + { id: "short_term", name: "Past Month" }, + { id: "medium_term", name: "Past 6 Months" }, + { id: "long_term", name: "All Time" } + ]; + var GenresPage = ({ configWrapper }) => { + const { LocalStorage } = Spicetify; + const [topGenres, setTopGenres] = import_react25.default.useState(100); + const [dropdown, activeOption] = useDropdownMenu_default(DropdownOptions3, "stats:top-genres"); + const fetchTopGenres = async (time_range, force, set = true, force_refetch) => { + if (!force) { + let storedData = LocalStorage.get(`stats:top-genres:${time_range}`); + if (storedData) + return setTopGenres(JSON.parse(storedData)); + } + const start = window.performance.now(); + const cacheInfo = JSON.parse(LocalStorage.get("stats:cache-info")); + const fetchedItems = await Promise.all( + ["artists", "tracks"].map(async (type, index) => { + if (cacheInfo[index] === true && !force_refetch) { + return await JSON.parse(LocalStorage.get(`stats:top-${type}:${time_range}`)); + } + const fetchedItems2 = await (type === "artists" ? topArtistsReq(time_range, configWrapper) : topTracksReq(time_range, configWrapper)); + cacheInfo[index] = true; + cacheInfo[2] = true; + LocalStorage.set(`stats:top-${type}:${time_range}`, JSON.stringify(fetchedItems2)); + LocalStorage.set("stats:cache-info", JSON.stringify(cacheInfo)); + return fetchedItems2; + }) + ); + for (let i = 0; i < 2; i++) { + if (fetchedItems[i] === 200 || fetchedItems[i] === 300) + return setTopGenres(fetchedItems[i]); + } + const fetchedArtists = fetchedItems[0].filter((artist) => artist?.genres); + const fetchedTracks = fetchedItems[1].filter((track) => track?.id); + const genres = fetchedArtists.reduce((acc, artist) => { + artist.genres.forEach((genre) => { + const index = acc.findIndex(([g]) => g === genre); + if (index !== -1) { + acc[index][1] += 1 * Math.abs(fetchedArtists.indexOf(artist) - 50); + } else { + acc.push([genre, 1 * Math.abs(fetchedArtists.indexOf(artist) - 50)]); + } + }); + return acc; + }, []); + let trackPopularity = 0; + let explicitness = 0; + let releaseData = []; + const topTracks = fetchedTracks.map((track) => { + trackPopularity += track.popularity; + if (track.explicit) + explicitness++; + if (track.release_year) { + const year = track.release_year; + const index = releaseData.findIndex(([y]) => y === year); + if (index !== -1) { + releaseData[index][1] += 1; + } else { + releaseData.push([year, 1]); + } + } + return track.id; + }); + async function testDupe(track) { + const spotifyItem = await apiRequest( + "track", + SPOTIFY.search(track.name, track.artists[0].name), + 1, + false + ).then((res) => res.tracks?.items); + if (!spotifyItem) + return false; + return spotifyItem.some((item) => { + return item.name === track.name && item.popularity > track.popularity; + }); + } + let obscureTracks2 = []; + for (let i = 0; i < fetchedTracks.length; i++) { + let track = fetchedTracks[i]; + if (!track?.popularity) + continue; + if (obscureTracks2.length < 5) { + const dupe = await testDupe(track); + if (dupe) + continue; + obscureTracks2.push(track); + obscureTracks2.sort((a, b) => b.popularity - a.popularity); + continue; + } + for (let j = 0; j < 5; j++) { + if (track.popularity < obscureTracks2[j].popularity) { + const dupe = await testDupe(track); + if (dupe) + break; + obscureTracks2.splice(j, 0, track); + obscureTracks2 = obscureTracks2.slice(0, 5); + break; + } + } + } + const fetchedFeatures = await fetchAudioFeatures(topTracks); + let audioFeatures = { + danceability: 0, + energy: 0, + valence: 0, + speechiness: 0, + acousticness: 0, + instrumentalness: 0, + liveness: 0, + tempo: 0 + }; + for (let i = 0; i < fetchedFeatures.length; i++) { + if (!fetchedFeatures[i]) + continue; + const track = fetchedFeatures[i]; + Object.keys(audioFeatures).forEach((feature) => { + audioFeatures[feature] += track[feature]; + }); + } + audioFeatures = { + popularity: trackPopularity, + explicitness, + ...audioFeatures + }; + for (let key in audioFeatures) { + audioFeatures[key] = audioFeatures[key] / 50; + } + console.log("total genres fetch time:", window.performance.now() - start); + if (set) + setTopGenres({ + genres, + features: audioFeatures, + years: releaseData, + obscureTracks: obscureTracks2 + }); + LocalStorage.set( + `stats:top-genres:${time_range}`, + JSON.stringify({ + genres, + features: audioFeatures, + years: releaseData, + obscureTracks: obscureTracks2 + }) + ); + }; + import_react25.default.useEffect(() => { + updatePageCache(2, fetchTopGenres, activeOption.id); + }, []); + import_react25.default.useEffect(() => { + fetchTopGenres(activeOption.id); + }, [activeOption]); + const refresh = () => { + fetchTopGenres(activeOption.id, true); + }; + const props = { + title: "Top Genres", + headerEls: [dropdown, /* @__PURE__ */ import_react25.default.createElement(refresh_button_default, { + callback: refresh + }), /* @__PURE__ */ import_react25.default.createElement(settings_button_default, { + configWrapper + })] + }; + switch (topGenres) { + case 300: + return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react25.default.createElement(status_default, { + icon: "error", + heading: "No API Key or Username", + subheading: "Please enter these in the settings menu" + })); + case 200: + return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react25.default.createElement(status_default, { + icon: "error", + heading: "Failed to Fetch Top Genres", + subheading: "An error occurred while fetching the data" + })); + case 100: + return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react25.default.createElement(status_default, { + icon: "library", + heading: "Loading", + subheading: "Fetching data..." + })); + } + const statCards = Object.entries(topGenres.features).map(([key, value]) => { + return /* @__PURE__ */ import_react25.default.createElement(stat_card_default, { + label: key, + value + }); + }); + const obscureTracks = topGenres.obscureTracks.map((track, index) => /* @__PURE__ */ import_react25.default.createElement(track_row_default, { + index: index + 1, + ...track, + uris: topGenres.obscureTracks.map((track2) => track2.uri) + })); + return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react25.default.createElement("section", { + className: "main-shelf-shelf Shelf" + }, /* @__PURE__ */ import_react25.default.createElement(genres_card_default, { + genres: topGenres.genres, + total: 1275 + }), /* @__PURE__ */ import_react25.default.createElement(inline_grid_default, { + special: true + }, statCards)), /* @__PURE__ */ import_react25.default.createElement(shelf_default, { + title: "Release Year Distribution" + }, /* @__PURE__ */ import_react25.default.createElement(genres_card_default, { + genres: topGenres.years, + total: 50 + })), /* @__PURE__ */ import_react25.default.createElement(shelf_default, { + title: "Most Obscure Tracks" + }, /* @__PURE__ */ import_react25.default.createElement(tracklist_default, { + minified: true + }, obscureTracks))); + }; + var top_genres_default = import_react25.default.memo(GenresPage); + + // src/pages/library.tsx + var import_react26 = __toESM(require_react()); + var DropdownOptions4 = [ + { id: "owned", name: "My Playlists" }, + { id: "all", name: "All Playlists" } + ]; + var LibraryPage = ({ configWrapper }) => { + const [library, setLibrary] = import_react26.default.useState(100); + const [dropdown, activeOption, setActiveOption] = useDropdownMenu_default(DropdownOptions4, "stats:library"); + const fetchData = async (option, force, set = true) => { + try { + if (!force) { + let storedData = Spicetify.LocalStorage.get(`stats:library:${option}`); + if (storedData) + return setLibrary(JSON.parse(storedData)); + } + const start = window.performance.now(); + const rootlistItems = await apiRequest("rootlist", SPOTIFY.rootlist); + const flattenPlaylists = (items) => { + const playlists2 = []; + items.forEach((row) => { + if (row.type === "playlist") { + playlists2.push(row); + } else if (row.type === "folder") { + if (!row.rows) + return; + const folderPlaylists = flattenPlaylists(row.rows); + playlists2.push(...folderPlaylists); + } + }); + return playlists2; + }; + let playlists = flattenPlaylists(rootlistItems?.rows); + playlists = playlists.sort((a, b) => a.ownedBySelf === b.ownedBySelf ? 0 : a.ownedBySelf ? -1 : 1); + let indexOfFirstNotOwned = -1; + let playlistUris = []; + let trackCount = 0; + let ownedTrackCount = 0; + playlists.forEach((playlist) => { + if (playlist.totalLength === 0) + return; + if (!playlist.ownedBySelf && indexOfFirstNotOwned === -1) + indexOfFirstNotOwned = playlistUris.length; + playlistUris.push(playlist.link); + trackCount += playlist.totalLength; + if (playlist.ownedBySelf) + ownedTrackCount += playlist.totalLength; + }, 0); + const playlistsMeta = await Promise.all( + playlistUris.map((uri) => { + return apiRequest("playlistsMetadata", SPOTIFY.playlist(uri), 5, false); + }) + ); + let duration = 0; + let trackIDs = []; + let popularity = 0; + let albums = {}; + let artists = {}; + let explicitCount = 0; + let ownedDuration = 0; + let ownedArtists = {}; + let ownedPopularity = 0; + let ownedAlbums = {}; + let ownedExplicitCount = 0; + for (let i = 0; i < playlistsMeta.length; i++) { + const playlist = playlistsMeta[i]; + if (!playlist) + continue; + if (i === indexOfFirstNotOwned) { + ownedDuration = duration; + ownedArtists = Object.assign({}, artists); + ownedPopularity = popularity; + ownedExplicitCount = explicitCount; + ownedAlbums = Object.assign({}, albums); + } + duration += playlist.playlist.duration; + playlist.items.forEach((track) => { + if (!track?.album) + return; + if (track.link.includes("local")) + return; + trackIDs.push(track.link.split(":")[2]); + if (track.isExplicit) + explicitCount++; + popularity += track.popularity; + const albumID = track.album.link.split(":")[2]; + albums[albumID] = albums[albumID] ? albums[albumID] + 1 : 1; + track.artists.forEach((artist) => { + const artistID = artist.link.split(":")[2]; + artists[artistID] = artists[artistID] ? artists[artistID] + 1 : 1; + }); + }); + } + const [topArtists, topGenres, topGenresTotal] = await fetchTopArtists(artists); + const [ownedTopArtists, ownedTopGenres, ownedTopGenresTotal] = await fetchTopArtists(ownedArtists); + const [topAlbums, releaseYears, releaseYearsTotal] = await fetchTopAlbums(albums); + const [ownedTopAlbums, ownedReleaseYears, ownedReleaseYearsTotal] = await fetchTopAlbums( + ownedAlbums, + topAlbums + ); + const fetchedFeatures = await fetchAudioFeatures(trackIDs); + const audioFeatures = { + danceability: 0, + energy: 0, + valence: 0, + speechiness: 0, + acousticness: 0, + instrumentalness: 0, + liveness: 0, + tempo: 0 + }; + let ownedAudioFeatures = {}; + for (let i = 0; i < fetchedFeatures.length; i++) { + if (i === ownedTrackCount) { + ownedAudioFeatures = { + popularity: ownedPopularity, + explicitness: ownedExplicitCount, + ...audioFeatures + }; + } + if (!fetchedFeatures[i]) + continue; + const track = fetchedFeatures[i]; + Object.keys(audioFeatures).forEach((feature) => { + audioFeatures[feature] += track[feature]; + }); + } + const allAudioFeatures = { + popularity, + explicitness: explicitCount, + ...audioFeatures + }; + for (let key in allAudioFeatures) { + allAudioFeatures[key] /= fetchedFeatures.length; + } + for (let key in ownedAudioFeatures) { + ownedAudioFeatures[key] /= ownedTrackCount; + } + const ownedStats = { + audioFeatures: ownedAudioFeatures, + trackCount: ownedTrackCount, + totalDuration: ownedDuration, + artists: ownedTopArtists, + artistCount: Object.keys(ownedArtists).length, + genres: ownedTopGenres, + genresDenominator: ownedTopGenresTotal, + playlistCount: indexOfFirstNotOwned > 0 ? indexOfFirstNotOwned : 0, + albums: ownedTopAlbums, + years: ownedReleaseYears, + yearsDenominator: ownedReleaseYearsTotal + }; + const allStats = { + playlistCount: playlists.length, + audioFeatures: allAudioFeatures, + trackCount, + totalDuration: duration, + artistCount: Object.keys(artists).length, + artists: topArtists, + genres: topGenres, + genresDenominator: topGenresTotal, + albums: topAlbums, + years: releaseYears, + yearsDenominator: releaseYearsTotal + }; + if (set) { + if (option === "all" && allStats.playlistCount) + setLibrary(allStats); + else if (option === "owned" && ownedStats.playlistCount) + setLibrary(ownedStats); + else + return setLibrary(300); + } + Spicetify.LocalStorage.set(`stats:library:all`, JSON.stringify(allStats)); + Spicetify.LocalStorage.set(`stats:library:owned`, JSON.stringify(ownedStats)); + console.log("total library fetch time:", window.performance.now() - start); + } catch (e) { + console.error(e); + setLibrary(200); + } + }; + import_react26.default.useEffect(() => { + updatePageCache(3, fetchData, activeOption.id, true); + }, []); + import_react26.default.useEffect(() => { + fetchData(activeOption.id); + }, [activeOption]); + const refresh = () => { + fetchData(activeOption.id, true); + }; + const props = { + title: "Library Analysis", + headerEls: [dropdown, /* @__PURE__ */ import_react26.default.createElement(refresh_button_default, { + callback: refresh + }), /* @__PURE__ */ import_react26.default.createElement(settings_button_default, { + configWrapper + })] + }; + switch (library) { + case 300: + return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react26.default.createElement(status_default, { + icon: "error", + heading: "No Playlists In Your Library", + subheading: "Try adding some playlists first" + })); + case 200: + return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react26.default.createElement(status_default, { + icon: "error", + heading: "Failed to Fetch Stats", + subheading: "Make an issue on Github" + })); + case 100: + return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react26.default.createElement(status_default, { + icon: "library", + heading: "Analysing your Library", + subheading: "This may take a while" + })); + } + const statCards = Object.entries(library.audioFeatures).map(([key, value]) => { + return /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { + label: key, + value + }); + }); + const artistCards = library.artists.slice(0, 10).map((artist) => { + return /* @__PURE__ */ import_react26.default.createElement(spotify_card_default, { + type: "artist", + uri: artist.uri, + header: artist.name, + subheader: `Appears in ${artist.freq} tracks`, + imageUrl: artist.image + }); + }); + const albumCards = library.albums.map((album) => { + return /* @__PURE__ */ import_react26.default.createElement(spotify_card_default, { + type: "album", + uri: album.uri, + header: album.name, + subheader: `Appears in ${album.freq} tracks`, + imageUrl: album.image + }); + }); + return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react26.default.createElement("section", { + className: "stats-libraryOverview" + }, /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { + label: "Total Playlists", + value: library.playlistCount.toString() + }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { + label: "Total Tracks", + value: library.trackCount.toString() + }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { + label: "Total Artists", + value: library.artistCount.toString() + }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { + label: "Total Minutes", + value: Math.floor(library.totalDuration / 60).toString() + }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { + label: "Total Hours", + value: (library.totalDuration / (60 * 60)).toFixed(1) + })), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { + title: "Most Frequent Genres" + }, /* @__PURE__ */ import_react26.default.createElement(genres_card_default, { + genres: library.genres, + total: library.genresDenominator + }), /* @__PURE__ */ import_react26.default.createElement(inline_grid_default, { + special: true + }, statCards)), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { + title: "Most Frequent Artists" + }, /* @__PURE__ */ import_react26.default.createElement(inline_grid_default, null, artistCards)), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { + title: "Most Frequent Albums" + }, /* @__PURE__ */ import_react26.default.createElement(inline_grid_default, null, albumCards)), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { + title: "Release Year Distribution" + }, /* @__PURE__ */ import_react26.default.createElement(genres_card_default, { + genres: library.years, + total: library.yearsDenominator + }))); + }; + var library_default = import_react26.default.memo(LibraryPage); + + // src/pages/charts.tsx + var import_react27 = __toESM(require_react()); + var DropdownOptions5 = [ + { id: "artists", name: "Top Artists" }, + { id: "tracks", name: "Top Tracks" } + ]; + var ChartsPage = ({ configWrapper }) => { + const [chartData, setChartData] = import_react27.default.useState(100); + const [dropdown, activeOption, setActiveOption] = useDropdownMenu_default(DropdownOptions5, "stats:charts"); + async function fetchChartData(type, force, set = true) { + if (!force) { + let storedData = Spicetify.LocalStorage.get(`stats:charts:${type}`); + if (storedData) + return setChartData(JSON.parse(storedData)); + } + const api_key = configWrapper.config["api-key"]; + if (!api_key) + return setChartData(200); + const response = await apiRequest("charts", LASTFM.charts(api_key, type)); + if (!response) + return setChartData(500); + const data = response[type].track || response[type].artist; + const cardData = await (type == "artists" ? convertArtistData(data) : convertTrackData(data)); + if (type === "tracks") { + const likedArray = await checkLiked(cardData.map((track) => track.id)); + if (!likedArray) + return setChartData(200); + cardData.forEach((track, index) => { + track.liked = likedArray[index]; + }); + } + if (set) + setChartData(cardData); + Spicetify.LocalStorage.set(`stats:charts:${type}`, JSON.stringify(cardData)); + } + import_react27.default.useEffect(() => { + updatePageCache(4, fetchChartData, activeOption.id, "charts"); + }, []); + import_react27.default.useEffect(() => { + fetchChartData(activeOption.id); + }, [activeOption]); + const refresh = () => { + fetchChartData(activeOption.id, true); + }; + const props = { + title: "Top Albums", + headerEls: [dropdown, /* @__PURE__ */ import_react27.default.createElement(refresh_button_default, { + callback: refresh + }), /* @__PURE__ */ import_react27.default.createElement(settings_button_default, { + configWrapper + })] + }; + switch (chartData) { + case 200: + return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react27.default.createElement(status_default, { + icon: "error", + heading: "No API Key", + subheading: "Please enter your Last.fm API key in the settings menu." + })); + case 500: + return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react27.default.createElement(status_default, { + icon: "error", + heading: "Error", + subheading: "An error occurred while fetching the data." + })); + case 100: + return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react27.default.createElement(status_default, { + icon: "library", + heading: "Loading", + subheading: "Fetching data from Last.fm..." + })); + } + if (!chartData[0]?.album) { + const artistCards = chartData.map((artist, index) => { + const type = artist.uri.startsWith("https") ? "lastfm" : "artist"; + return /* @__PURE__ */ import_react27.default.createElement(spotify_card_default, { + type, + uri: artist.uri, + header: artist.name, + subheader: `#${index + 1} Artist`, + imageUrl: artist.image + }); + }); + props.title = `Charts - Top Artists`; + return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react27.default.createElement("div", { + className: `main-gridContainer-gridContainer grid` + }, artistCards)); + } else { + const date = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit" + }); + const infoToCreatePlaylist = { + playlistName: `Charts - Top Tracks - ${date}`, + itemsUris: chartData.map((track) => track.uri) + }; + const trackRows = chartData.map((track, index) => /* @__PURE__ */ import_react27.default.createElement(track_row_default, { + index: index + 1, + ...track, + uris: chartData.map((track2) => track2.uri) + })); + props.title = `Charts - Top Tracks`; + return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { + ...props, + infoToCreatePlaylist + }, /* @__PURE__ */ import_react27.default.createElement(tracklist_default, null, trackRows)); + } + }; + var charts_default = import_react27.default.memo(ChartsPage); + + // src/pages/top_albums.tsx + var import_react28 = __toESM(require_react()); + var topAlbumsReq = async (time_range, configWrapper) => { + const { config } = configWrapper; + if (!config["api-key"] || !config["lastfm-user"]) + return 300; + const { ["lastfm-user"]: user, ["api-key"]: key } = config; + const response = await apiRequest("lastfm", LASTFM.topalbums(user, key, time_range)); + if (!response) + return 200; + return await convertAlbumData(response.topalbums.album); + }; + var DropdownOptions6 = [ + { id: "short_term", name: "Past Month" }, + { id: "medium_term", name: "Past 6 Months" }, + { id: "long_term", name: "All Time" } + ]; + var AlbumsPage = ({ configWrapper }) => { + const { LocalStorage } = Spicetify; + const [topAlbums, setTopAlbums] = import_react28.default.useState(100); + const [dropdown, activeOption] = useDropdownMenu_default(DropdownOptions6, "stats:top-albums"); + const fetchTopAlbums2 = async (time_range, force, set = true) => { + if (!force) { + let storedData = LocalStorage.get(`stats:top-albums:${time_range}`); + if (storedData) + return setTopAlbums(JSON.parse(storedData)); + } + const start = window.performance.now(); + const topAlbums2 = await topAlbumsReq(time_range, configWrapper); + if (set) + setTopAlbums(topAlbums2); + LocalStorage.set(`stats:top-albums:${time_range}`, JSON.stringify(topAlbums2)); + console.log("total albums fetch time:", window.performance.now() - start); + }; + import_react28.default.useEffect(() => { + updatePageCache(5, fetchTopAlbums2, activeOption.id); + }, []); + import_react28.default.useEffect(() => { + fetchTopAlbums2(activeOption.id); + }, [activeOption]); + const refresh = () => { + fetchTopAlbums2(activeOption.id, true); + }; + const props = { + title: "Top Albums", + headerEls: [dropdown, /* @__PURE__ */ import_react28.default.createElement(refresh_button_default, { + callback: refresh + }), /* @__PURE__ */ import_react28.default.createElement(settings_button_default, { + configWrapper + })] + }; + switch (topAlbums) { + case 300: + return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react28.default.createElement(status_default, { + icon: "error", + heading: "No API Key or Username", + subheading: "Please enter these in the settings menu" + })); + case 200: + return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react28.default.createElement(status_default, { + icon: "error", + heading: "Failed to Fetch Top Artists", + subheading: "An error occurred while fetching the data" + })); + case 100: + return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react28.default.createElement(status_default, { + icon: "library", + heading: "Loading", + subheading: "Fetching data..." + })); + } + const albumCards = topAlbums.map((album, index) => { + const type = album.uri.startsWith("https") ? "lastfm" : "album"; + return /* @__PURE__ */ import_react28.default.createElement(spotify_card_default, { + type, + uri: album.uri, + header: album.name, + subheader: `#${index + 1} Album`, + imageUrl: album.image + }); + }); + return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { + ...props + }, /* @__PURE__ */ import_react28.default.createElement("div", { + className: `main-gridContainer-gridContainer grid` + }, albumCards)); + }; + var top_albums_default = import_react28.default.memo(AlbumsPage); + + // package.json + var version = "0.3.3"; + + // src/app.tsx + var checkForUpdates = (setNewUpdate) => { + fetch("https://api.github.com/repos/harbassan/spicetify-apps/releases").then((res) => res.json()).then( + (result) => { + const releases = result.filter((release) => release.name.startsWith("stats")); + setNewUpdate(releases[0].name.slice(7) !== version); + }, + (error) => { + console.log("Failed to check for updates", error); + } + ); + }; + var App = () => { + const [config, setConfig] = import_react29.default.useState({ ...SpicetifyStats.ConfigWrapper.Config }); + const launchModal = () => { + SpicetifyStats.ConfigWrapper.launchModal(setConfig); + }; + const configWrapper = { + config, + launchModal + }; + const pages = { + ["Artists"]: /* @__PURE__ */ import_react29.default.createElement(top_artists_default, { + configWrapper + }), + ["Tracks"]: /* @__PURE__ */ import_react29.default.createElement(top_tracks_default, { + configWrapper + }), + ["Albums"]: /* @__PURE__ */ import_react29.default.createElement(top_albums_default, { + configWrapper + }), + ["Genres"]: /* @__PURE__ */ import_react29.default.createElement(top_genres_default, { + configWrapper + }), + ["Library"]: /* @__PURE__ */ import_react29.default.createElement(library_default, { + configWrapper + }), + ["Charts"]: /* @__PURE__ */ import_react29.default.createElement(charts_default, { + configWrapper + }) + }; + const tabPages = ["Artists", "Tracks", "Albums", "Genres", "Library", "Charts"].filter( + (page) => configWrapper.config[`show-${page.toLowerCase()}`] + ); + const [navBar, activeLink, setActiveLink] = useNavigationBar_default(tabPages); + const [hasPageSwitched, setHasPageSwitched] = import_react29.default.useState(false); + const [newUpdate, setNewUpdate] = import_react29.default.useState(false); + import_react29.default.useEffect(() => { + setActiveLink(Spicetify.LocalStorage.get("stats:active-link") || "Artists"); + checkForUpdates(setNewUpdate); + setHasPageSwitched(true); + }, []); + import_react29.default.useEffect(() => { + Spicetify.LocalStorage.set("stats:active-link", activeLink); + }, [activeLink]); + if (!hasPageSwitched) { + return /* @__PURE__ */ import_react29.default.createElement(import_react29.default.Fragment, null); + } + return /* @__PURE__ */ import_react29.default.createElement("div", { + id: "stats-app" + }, navBar, newUpdate && /* @__PURE__ */ import_react29.default.createElement("div", { + className: "new-update" + }, "New app update available! Visit", " ", /* @__PURE__ */ import_react29.default.createElement("a", { + href: "https://github.com/harbassan/spicetify-apps/releases" + }, "harbassan/spicetify-apps"), " to install."), pages[activeLink]); + }; + var app_default = App; + + // ../../../AppData/Local/Temp/spicetify-creator/index.jsx + var import_react30 = __toESM(require_react()); + function render() { + return /* @__PURE__ */ import_react30.default.createElement(app_default, null); + } + return __toCommonJS(spicetify_creator_exports); +})(); +const render=()=>stats.default(); |
