dotfiles/.config/spicetify/CustomApps/library/extension.js
2025-04-20 14:03:02 +02:00

1037 lines
No EOL
41 KiB
JavaScript

(async function() {
while (!Spicetify.React || !Spicetify.ReactDOM) {
await new Promise(resolve => setTimeout(resolve, 10));
}
"use strict";
var library = (() => {
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 __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
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 __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
// 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;
}
});
// ../shared/config/config_wrapper.tsx
var import_react2 = __toESM(require_react());
// ../shared/config/config_modal.tsx
var import_react = __toESM(require_react());
var TextInput = (props) => {
const handleTextChange = (event) => {
props.callback(event.target.value);
};
return /* @__PURE__ */ import_react.default.createElement("label", {
className: "text-input-wrapper"
}, /* @__PURE__ */ import_react.default.createElement("input", {
className: "text-input",
type: "text",
value: props.value || "",
"data-storage-key": props.storageKey,
placeholder: props.placeholder,
id: `text-input:${props.storageKey}`,
title: `Text input for ${props.storageKey}`,
onChange: handleTextChange
}));
};
var Dropdown = (props) => {
const handleDropdownChange = (event) => {
props.callback(event.target.value);
};
return /* @__PURE__ */ import_react.default.createElement("label", {
className: "dropdown-wrapper"
}, /* @__PURE__ */ import_react.default.createElement("select", {
className: "dropdown-input",
value: props.value,
"data-storage-key": props.storageKey,
id: `dropdown:${props.storageKey}`,
title: `Dropdown for ${props.storageKey}`,
onChange: handleDropdownChange
}, props.options.map((option, index) => /* @__PURE__ */ import_react.default.createElement("option", {
key: index,
value: option
}, option))));
};
var ToggleInput = (props) => {
const { Toggle } = Spicetify.ReactComponent;
const handleToggleChange = (newValue) => {
props.callback(newValue);
};
return /* @__PURE__ */ import_react.default.createElement(Toggle, {
id: `toggle:${props.storageKey}`,
value: props.value,
onSelected: (newValue) => handleToggleChange(newValue)
});
};
var SliderInput = (props) => {
const { Slider } = Spicetify.ReactComponent;
const [value, setValue] = import_react.default.useState((props.value - props.min) / (props.max - props.min));
const handleSliderChange = import_react.default.useCallback((newValue) => {
setValue(newValue);
const calculatedValue = props.min + newValue * (props.max - props.min);
props.callback(calculatedValue);
}, [props]);
const handleDragMove = import_react.default.useCallback((v) => {
console.log(v);
}, []);
return /* @__PURE__ */ import_react.default.createElement(Slider, {
id: `slider:${props.storageKey}`,
value,
min: 0,
max: 1,
step: 0.1,
onDragMove: handleDragMove,
onDragStart: handleSliderChange,
onDragEnd: () => {
}
});
};
var TooltipIcon = () => {
return /* @__PURE__ */ import_react.default.createElement("svg", {
role: "img",
height: "16",
width: "16",
className: "Svg-sc-ytk21e-0 uPxdw nW1RKQOkzcJcX6aDCZB4",
viewBox: "0 0 16 16"
}, /* @__PURE__ */ import_react.default.createElement("path", {
d: "M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8z"
}), /* @__PURE__ */ import_react.default.createElement("path", {
d: "M7.25 12.026v-1.5h1.5v1.5h-1.5zm.884-7.096A1.125 1.125 0 007.06 6.39l-1.431.448a2.625 2.625 0 115.13-.784c0 .54-.156 1.015-.503 1.488-.3.408-.7.652-.973.818l-.112.068c-.185.116-.26.203-.302.283-.046.087-.097.245-.097.57h-1.5c0-.47.072-.898.274-1.277.206-.385.507-.645.827-.846l.147-.092c.285-.177.413-.257.526-.41.169-.23.213-.397.213-.602 0-.622-.503-1.125-1.125-1.125z"
}));
};
var ConfigRow = (props) => {
return /* @__PURE__ */ import_react.default.createElement("div", {
className: "setting-row"
}, /* @__PURE__ */ import_react.default.createElement("label", {
className: "col description"
}, props.name, props.desc && /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.TooltipWrapper, {
label: /* @__PURE__ */ import_react.default.createElement("div", {
dangerouslySetInnerHTML: { __html: props.desc }
}),
renderInline: true,
showDelay: 10,
placement: "top",
labelClassName: "tooltip",
disabled: false
}, /* @__PURE__ */ import_react.default.createElement("div", {
className: "tooltip-icon"
}, /* @__PURE__ */ import_react.default.createElement(TooltipIcon, null)))), /* @__PURE__ */ import_react.default.createElement("div", {
className: "col action"
}, props.children));
};
var ConfigModal = (props) => {
const { config, structure, appKey, updateAppConfig } = props;
const [modalConfig, setModalConfig] = import_react.default.useState({ ...config });
const modalRows = structure.map((modalRow, index) => {
const key = modalRow.key;
const currentValue = modalConfig[key];
const updateItem = (state) => {
console.debug(`toggling ${key} to ${state}`);
localStorage.setItem(`${appKey}:config:${key}`, String(state));
if (modalRow.callback)
modalRow.callback(state);
const newConfig = { ...modalConfig };
newConfig[key] = state;
updateAppConfig(newConfig);
setModalConfig(newConfig);
};
const header = modalRow.sectionHeader;
const element = () => {
switch (modalRow.type) {
case "toggle":
return /* @__PURE__ */ import_react.default.createElement(ToggleInput, {
storageKey: key,
value: currentValue,
callback: updateItem
});
case "text":
return /* @__PURE__ */ import_react.default.createElement(TextInput, {
storageKey: key,
value: currentValue,
callback: updateItem
});
case "dropdown":
return /* @__PURE__ */ import_react.default.createElement(Dropdown, {
storageKey: key,
value: currentValue,
options: modalRow.options,
callback: updateItem
});
case "slider":
return /* @__PURE__ */ import_react.default.createElement(SliderInput, {
storageKey: key,
value: currentValue,
min: modalRow.min,
max: modalRow.max,
step: modalRow.step,
callback: updateItem
});
}
};
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, header && index !== 0 && /* @__PURE__ */ import_react.default.createElement("br", null), header && /* @__PURE__ */ import_react.default.createElement("h2", {
className: "section-header"
}, modalRow.sectionHeader), /* @__PURE__ */ import_react.default.createElement(ConfigRow, {
name: modalRow.name,
desc: modalRow.desc
}, element()));
});
return /* @__PURE__ */ import_react.default.createElement("div", {
className: "config-container"
}, modalRows);
};
var config_modal_default = ConfigModal;
// ../shared/config/config_wrapper.tsx
var _ConfigWrapper = class {
Config;
launchModal;
constructor(modalStructure, key) {
const config = modalStructure.map((modalStructureRow) => {
const value = _ConfigWrapper.getLocalStorageDataFromKey(
`${key}:config:${modalStructureRow.key}`,
modalStructureRow.def
);
modalStructureRow.callback?.(value);
return { [modalStructureRow.key]: value };
});
this.Config = Object.assign({}, ...config);
this.launchModal = (callback) => {
const updateConfig = (config2) => {
this.Config = { ...config2 };
callback?.(config2);
};
Spicetify.PopupModal.display({
title: `${key.charAt(0).toUpperCase() + key.slice(1)} Settings`,
content: /* @__PURE__ */ import_react2.default.createElement(config_modal_default, {
config: this.Config,
structure: modalStructure,
appKey: key,
updateAppConfig: updateConfig
}),
isLarge: true
});
};
}
};
var ConfigWrapper = _ConfigWrapper;
__publicField(ConfigWrapper, "getLocalStorageDataFromKey", (key, fallback) => {
const data = localStorage.getItem(key);
if (data) {
try {
return JSON.parse(data);
} catch (err) {
return data;
}
} else {
return fallback;
}
});
var config_wrapper_default = ConfigWrapper;
// src/extensions/extension.tsx
var import_react10 = __toESM(require_react());
var import_react_dom = __toESM(require_react_dom());
// src/components/toggle_filters.tsx
var import_react3 = __toESM(require_react());
var UpIcon = () => {
const { IconComponent } = Spicetify.ReactComponent;
return /* @__PURE__ */ import_react3.default.createElement(IconComponent, {
semanticColor: "textSubdued",
dangerouslySetInnerHTML: {
__html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M.998 8.81A.749.749 0 0 1 .47 7.53L7.99 0l7.522 7.53a.75.75 0 1 1-1.06 1.06L8.74 2.87v12.38a.75.75 0 1 1-1.498 0V2.87L1.528 8.59a.751.751 0 0 1-.53.22z"></path></svg>'
},
iconSize: 16
});
};
var DownIcon = () => {
const { IconComponent } = Spicetify.ReactComponent;
return /* @__PURE__ */ import_react3.default.createElement(IconComponent, {
semanticColor: "textSubdued",
dangerouslySetInnerHTML: {
__html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M.998 7.19A.749.749 0 0 0 .47 8.47L7.99 16l7.522-7.53a.75.75 0 1 0-1.06-1.06L8.74 13.13V.75a.75.75 0 1 0-1.498 0v12.38L1.528 7.41a.749.749 0 0 0-.53-.22z"></path></svg>'
},
iconSize: 16
});
};
var ToggleFiltersButton = () => {
const [direction, setDirection] = import_react3.default.useState(
document.body.classList.contains("show-ylx-filters") ? "up" : "down"
);
const { ButtonTertiary } = Spicetify.ReactComponent;
const toggleDirection = () => {
if (direction === "down") {
document.body.classList.add("show-ylx-filters");
setDirection("up");
} else {
setDirection("down");
document.body.classList.remove("show-ylx-filters");
}
};
const Icon = direction === "down" ? DownIcon : UpIcon;
return /* @__PURE__ */ import_react3.default.createElement(ButtonTertiary, {
buttonSize: "sm",
"aria-label": "Show Filters",
iconOnly: Icon,
onClick: toggleDirection
});
};
var toggle_filters_default = ToggleFiltersButton;
// src/components/collapse_button.tsx
var import_react4 = __toESM(require_react());
var collapseLibrary = () => {
Spicetify.Platform.LocalStorageAPI.setItem("left-sidebar-state", 1);
};
var CollapseIcon = () => {
const { IconComponent } = Spicetify.ReactComponent;
return /* @__PURE__ */ import_react4.default.createElement(IconComponent, {
semanticColor: "textSubdued",
dangerouslySetInnerHTML: {
__html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8.81 1A.749.749 0 0 0 7.53.47L0 7.99l7.53 7.521a.75.75 0 0 0 1.234-.815.75.75 0 0 0-.174-.243L2.87 8.74h12.38a.75.75 0 1 0 0-1.498H2.87l5.72-5.713c.14-.14.22-.331.22-.53z"></path></svg>'
},
iconSize: 16
});
};
var CollapseButton = () => {
const { ButtonTertiary } = Spicetify.ReactComponent;
return /* @__PURE__ */ import_react4.default.createElement(ButtonTertiary, {
buttonSize: "sm",
"aria-label": "Show Filters",
iconOnly: CollapseIcon,
onClick: collapseLibrary
});
};
var collapse_button_default = CollapseButton;
// src/components/album_menu_item.tsx
var import_react8 = __toESM(require_react());
// src/components/leading_icon.tsx
var import_react5 = __toESM(require_react());
var LeadingIcon = ({ path }) => {
return /* @__PURE__ */ import_react5.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;
// src/components/text_input_dialog.tsx
var import_react6 = __toESM(require_react());
var TextInputDialog = (props) => {
const { def, placeholder, onSave } = props;
const [value, setValue] = import_react6.default.useState(def || "");
const onSubmit = (e) => {
e.preventDefault();
Spicetify.PopupModal.hide();
onSave(value);
};
return /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, /* @__PURE__ */ import_react6.default.createElement("form", {
className: "text-input-form",
onSubmit
}, /* @__PURE__ */ import_react6.default.createElement("label", {
className: "text-input-wrapper"
}, /* @__PURE__ */ import_react6.default.createElement("input", {
className: "text-input",
type: "text",
value,
placeholder,
onChange: (e) => setValue(e.target.value)
})), /* @__PURE__ */ import_react6.default.createElement("button", {
type: "submit",
"data-encore-id": "buttonPrimary",
className: "Button-sc-qlcn5g-0 Button-small-buttonPrimary"
}, /* @__PURE__ */ import_react6.default.createElement("span", {
className: "ButtonInner-sc-14ud5tc-0 ButtonInner-small encore-bright-accent-set"
}, "Save"))));
};
var text_input_dialog_default = TextInputDialog;
// src/components/searchbar.tsx
var import_react7 = __toESM(require_react());
var SearchBar = (props) => {
const { setSearch, placeholder } = props;
const handleChange = (e) => {
setSearch(e.target.value);
};
return /* @__PURE__ */ import_react7.default.createElement("div", {
className: "x-filterBox-filterInputContainer x-filterBox-expandedOrHasFilter",
role: "search"
}, /* @__PURE__ */ import_react7.default.createElement("input", {
type: "text",
className: "x-filterBox-filterInput",
role: "searchbox",
maxLength: 80,
autoCorrect: "off",
autoCapitalize: "off",
spellCheck: "false",
placeholder: `Search ${placeholder}`,
"aria-hidden": "false",
onChange: handleChange
}), /* @__PURE__ */ import_react7.default.createElement("div", {
className: "x-filterBox-overlay"
}, /* @__PURE__ */ import_react7.default.createElement("span", {
className: "x-filterBox-searchIconContainer"
}, /* @__PURE__ */ import_react7.default.createElement("svg", {
"data-encore-id": "icon",
role: "img",
"aria-hidden": "true",
className: "Svg-sc-ytk21e-0 Svg-img-icon-small e-9640-icon x-filterBox-searchIcon",
viewBox: "0 0 16 16"
}, /* @__PURE__ */ import_react7.default.createElement("path", {
d: "M7 1.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5zM.25 7a6.75 6.75 0 1 1 12.096 4.12l3.184 3.185a.75.75 0 1 1-1.06 1.06L11.304 12.2A6.75 6.75 0 0 1 .25 7z"
})))), /* @__PURE__ */ import_react7.default.createElement("button", {
className: "x-filterBox-expandButton",
"aria-hidden": "false",
"aria-label": "Search Playlists",
type: "button"
}, /* @__PURE__ */ import_react7.default.createElement("svg", {
"data-encore-id": "icon",
role: "img",
"aria-hidden": "true",
className: "Svg-sc-ytk21e-0 Svg-img-icon-small x-filterBox-searchIcon",
viewBox: "0 0 16 16"
}, /* @__PURE__ */ import_react7.default.createElement("path", {
d: "M7 1.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5zM.25 7a6.75 6.75 0 1 1 12.096 4.12l3.184 3.185a.75.75 0 1 1-1.06 1.06L11.304 12.2A6.75 6.75 0 0 1 .25 7z"
}))));
};
var searchbar_default = SearchBar;
// src/components/album_menu_item.tsx
var createCollection = () => {
const onSave = (value) => {
CollectionsWrapper.createCollection(value);
};
Spicetify.PopupModal.display({
title: "Create Collection",
content: /* @__PURE__ */ import_react8.default.createElement(text_input_dialog_default, {
def: "New Collection",
placeholder: "Collection Name",
onSave
})
});
};
var CollectionSearchMenu = () => {
const { MenuItem } = Spicetify.ReactComponent;
const { SVGIcons } = Spicetify;
const [textFilter, setTextFilter] = import_react8.default.useState("");
const [collections, setCollections] = import_react8.default.useState(null);
const context = import_react8.default.useContext(Spicetify.ContextMenuV2._context);
const uri = context?.props?.uri || context?.props?.id;
import_react8.default.useEffect(() => {
const fetchCollections = async () => {
setCollections(await CollectionsWrapper.getContents({ textFilter, limit: 20, offset: 0 }));
};
fetchCollections();
}, [textFilter]);
if (!collections)
return /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null);
const addToCollection = (collectionUri) => {
CollectionsWrapper.addAlbumToCollection(collectionUri, uri);
};
const activeCollections = CollectionsWrapper.getCollectionsWithAlbum(uri);
const hasCollections = activeCollections.length > 0;
const removeFromCollections = () => {
for (const collection of activeCollections) {
CollectionsWrapper.removeAlbumFromCollection(collection.uri, uri);
}
};
const allCollectionsLength = collections.totalLength;
const menuItems = collections.items.map((collection, index) => {
return /* @__PURE__ */ import_react8.default.createElement(MenuItem, {
key: collection.uri,
onClick: () => {
addToCollection(collection.uri);
},
divider: index === 0 ? "before" : void 0
}, collection.name);
});
const menuLength = allCollectionsLength + (hasCollections ? 1 : 0);
return /* @__PURE__ */ import_react8.default.createElement("div", {
className: "main-contextMenu-filterPlaylistSearchContainer",
style: { "--context-menu-submenu-length": `${menuLength}` }
}, /* @__PURE__ */ import_react8.default.createElement("li", {
role: "presentation",
className: "main-contextMenu-filterPlaylistSearch"
}, /* @__PURE__ */ import_react8.default.createElement("div", {
role: "menuitem"
}, /* @__PURE__ */ import_react8.default.createElement(searchbar_default, {
setSearch: setTextFilter,
placeholder: "collections"
}))), /* @__PURE__ */ import_react8.default.createElement(MenuItem, {
key: "new-collection",
leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, {
path: SVGIcons.plus2px
}),
onClick: createCollection
}, "Create collection"), hasCollections && /* @__PURE__ */ import_react8.default.createElement(MenuItem, {
key: "remove-collection",
leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, {
path: SVGIcons.minus
}),
onClick: removeFromCollections
}, "Remove from all"), menuItems);
};
var AlbumMenuItem = () => {
const { MenuSubMenuItem } = Spicetify.ReactComponent;
const { SVGIcons } = Spicetify;
return /* @__PURE__ */ import_react8.default.createElement(MenuSubMenuItem, {
displayText: "Add to collection",
divider: "after",
leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, {
path: SVGIcons.plus2px
})
}, /* @__PURE__ */ import_react8.default.createElement(CollectionSearchMenu, null));
};
var album_menu_item_default = AlbumMenuItem;
// src/components/artist_menu_item.tsx
var import_react9 = __toESM(require_react());
var ArtistMenuItem = () => {
const { MenuItem } = Spicetify.ReactComponent;
const { SVGIcons } = Spicetify;
const context = import_react9.default.useContext(Spicetify.ContextMenuV2._context);
const uri = context?.props?.uri;
return /* @__PURE__ */ import_react9.default.createElement(MenuItem, {
divider: "after",
leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, {
path: SVGIcons.plus2px
}),
onClick: () => CollectionsWrapper.createCollectionFromDiscog(uri)
}, "Create Discog Collection");
};
var artist_menu_item_default = ArtistMenuItem;
// ../node_modules/uuid/dist/esm-browser/rng.js
var getRandomValues;
var rnds8 = new Uint8Array(16);
function rng() {
if (!getRandomValues) {
getRandomValues = typeof crypto !== "undefined" && crypto.getRandomValues && crypto.getRandomValues.bind(crypto);
if (!getRandomValues) {
throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
}
}
return getRandomValues(rnds8);
}
// ../node_modules/uuid/dist/esm-browser/stringify.js
var byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 256).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
return byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]];
}
// ../node_modules/uuid/dist/esm-browser/native.js
var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
var native_default = {
randomUUID
};
// ../node_modules/uuid/dist/esm-browser/v4.js
function v4(options, buf, offset) {
if (native_default.randomUUID && !buf && !options) {
return native_default.randomUUID();
}
options = options || {};
const rnds = options.random || (options.rng || rng)();
rnds[6] = rnds[6] & 15 | 64;
rnds[8] = rnds[8] & 63 | 128;
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
var v4_default = v4;
// src/extensions/collections_wrapper.ts
var _CollectionsWrapper = class extends EventTarget {
_collections;
constructor() {
super();
this._collections = JSON.parse(localStorage.getItem("library:collections") || "[]");
}
saveCollections() {
localStorage.setItem("library:collections", JSON.stringify(this._collections));
this.dispatchEvent(new CustomEvent("update", { detail: this._collections }));
}
getCollection(uri) {
return this._collections.find((collection) => collection.uri === uri);
}
async getLocalAlbums() {
const localAlbumsIntegration = window.localTracksService;
if (!localAlbumsIntegration)
return /* @__PURE__ */ new Map();
if (!localAlbumsIntegration.isReady) {
await new Promise((resolve) => {
const sub = localAlbumsIntegration.isReady$.subscribe((ready) => {
if (ready) {
resolve(true);
sub.unsubscribe();
}
});
localAlbumsIntegration.init();
});
}
return localAlbumsIntegration.getAlbums();
}
async getCollectionContents(uri) {
const collection = this.getCollection(uri);
if (!collection)
throw new Error("Collection not found");
const items = this._collections.filter((collection2) => collection2.parentCollection === uri);
const albums = await Spicetify.Platform.LibraryAPI.getContents({
filters: ["0"],
offset: 0,
limit: 9999
});
items.push(...albums.items.filter((album) => collection.items.includes(album.uri)));
const localAlbumUris = collection.items.filter((item) => item.includes("local"));
if (localAlbumUris.length > 0) {
const localAlbums = await this.getLocalAlbums();
const inCollection = localAlbumUris.map((uri2) => localAlbums.get(uri2));
items.push(...inCollection.filter(Boolean));
}
return items;
}
async getContents(props) {
const { collectionUri, offset, limit, textFilter } = props;
let items = collectionUri ? await this.getCollectionContents(collectionUri) : this._collections;
const openedCollectionName = collectionUri ? this.getCollection(collectionUri)?.name : void 0;
if (textFilter) {
const regex = new RegExp(`\\b${textFilter}`, "i");
items = items.filter((collection) => regex.test(collection.name));
}
items = items.slice(offset, offset + limit);
return { items, totalLength: this._collections.length, offset, openedCollectionName };
}
async cleanCollections() {
for (const collection of this._collections) {
const boolArray = await Spicetify.Platform.LibraryAPI.contains(...collection.items);
if (boolArray.includes(false)) {
const result = [];
for (let i = 0; i < boolArray.length; i++) {
if (boolArray[i] || collection.items[i].includes("local")) {
result.push(collection.items[i]);
}
}
if (result.length !== collection.items.length) {
collection.items = collection.items.filter((uri, i) => boolArray[i] || uri.includes("local"));
this.saveCollections();
Spicetify.showNotification("Album removed from collection");
this.syncCollection(collection.uri);
}
}
}
}
async syncCollection(uri) {
const collection = this.getCollection(uri);
if (!collection)
return;
const { PlaylistAPI } = Spicetify.Platform;
if (!collection.syncedPlaylistUri)
return;
const playlist = await PlaylistAPI.getPlaylist(collection.syncedPlaylistUri);
const playlistTracks = playlist.contents.items.filter((t) => t.type === "track").map((t) => t.uri);
const collectionTracks = await this.getTracklist(uri);
const wanted = collectionTracks.filter((track) => !playlistTracks.includes(track));
const unwanted = playlistTracks.filter((track) => !collectionTracks.includes(track)).map((uri2) => ({ uri: uri2, uid: [] }));
if (wanted.length)
await PlaylistAPI.add(collection.syncedPlaylistUri, wanted, { before: "end" });
if (unwanted.length)
await PlaylistAPI.remove(collection.syncedPlaylistUri, unwanted);
Spicetify.showNotification("Playlist synced");
}
unsyncCollection(uri) {
const collection = this.getCollection(uri);
if (!collection)
return;
collection.syncedPlaylistUri = void 0;
this.saveCollections();
Spicetify.showNotification("Collection unsynced");
}
async getTracklist(collectionUri) {
const collection = this.getCollection(collectionUri);
if (!collection)
return [];
return Promise.all(
collection.items.map(async (uri) => {
if (uri.includes("local")) {
const localAlbums = await this.getLocalAlbums();
const localAlbum = localAlbums.get(uri);
return localAlbum?.getTracks().map((t) => t.uri) || [];
}
const res = await Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryAlbumTrackUris, {
offset: 0,
limit: 50,
uri
});
return res.data.albumUnion.tracksV2.items.map((t) => t.track.uri);
})
).then((tracks) => tracks.flat());
}
async convertToPlaylist(uri) {
const collection = this.getCollection(uri);
if (!collection)
return;
const { Platform, showNotification } = Spicetify;
const { RootlistAPI, PlaylistAPI } = Platform;
if (collection.syncedPlaylistUri) {
showNotification("Synced Playlist already exists", true);
return;
}
try {
const playlistUri = await RootlistAPI.createPlaylist(collection.name, { before: "start" });
const items = await this.getTracklist(uri);
await PlaylistAPI.add(playlistUri, items, { before: "start" });
collection.syncedPlaylistUri = playlistUri;
} catch (error) {
console.error(error);
showNotification("Failed to create playlist", true);
}
}
async createCollectionFromDiscog(artistUri) {
const [raw, info] = await Promise.all([
Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryArtistDiscographyAlbums, {
uri: artistUri,
offset: 0,
limit: 50,
order: "DATE_DESC"
}),
Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.queryArtistOverview, {
uri: artistUri,
locale: Spicetify.Locale.getLocale(),
includePrerelease: false
})
]);
const items = raw?.data?.artistUnion.discography.albums?.items;
const name = info?.data?.artistUnion.profile.name;
const image = info?.data?.artistUnion.visuals.avatarImage?.sources?.[0]?.url;
if (!name || !items?.length) {
Spicetify.showNotification("Artist not found or has no albums");
return;
}
const collectionUri = this.createCollection(`${name} Albums`);
if (image)
this.setCollectionImage(collectionUri, image);
for (const album of items) {
this.addAlbumToCollection(collectionUri, album.releases.items[0].uri);
}
}
createCollection(name, parentCollection = "") {
const id = v4_default();
this._collections.push({
type: "collection",
uri: id,
name,
items: [],
addedAt: new Date(),
lastPlayedAt: new Date(),
parentCollection
});
this.saveCollections();
Spicetify.showNotification("Collection created");
return id;
}
deleteCollection(uri) {
this._collections = this._collections.filter((collection) => collection.uri !== uri);
this.saveCollections();
Spicetify.showNotification("Collection deleted");
}
deleteCollectionAndAlbums(uri) {
const collection = this.getCollection(uri);
if (!collection)
return;
for (const album of collection.items) {
if (!album.includes("local"))
Spicetify.Platform.LibraryAPI.remove({ uris: [album] });
}
this.deleteCollection(uri);
}
async addAlbumToCollection(collectionUri, albumUri) {
const collection = this.getCollection(collectionUri);
if (!collection)
return;
if (!albumUri.includes("local")) {
const isSaved = await Spicetify.Platform.LibraryAPI.contains(albumUri)[0];
if (!isSaved) {
await Spicetify.Platform.LibraryAPI.add({ uris: [albumUri] });
}
}
if (!collection.items.includes(albumUri)) {
collection.items.push(albumUri);
this.saveCollections();
Spicetify.showNotification("Album added to collection");
this.syncCollection(collectionUri);
} else {
Spicetify.showNotification("Album already in collection");
}
}
removeAlbumFromCollection(collectionUri, albumUri) {
const collection = this.getCollection(collectionUri);
if (!collection)
return;
collection.items = collection.items.filter((item) => item !== albumUri);
this.saveCollections();
Spicetify.showNotification("Album removed from collection");
this.syncCollection(collectionUri);
}
getCollectionsWithAlbum(albumUri) {
return this._collections.filter((collection) => {
return collection.items.some((item) => item === albumUri);
});
}
renameCollection(uri, name) {
const collection = this.getCollection(uri);
if (!collection)
return;
collection.name = name;
this.saveCollections();
Spicetify.showNotification("Collection renamed");
}
setCollectionImage(uri, url) {
const collection = this.getCollection(uri);
if (!collection)
return;
collection.image = url;
this.saveCollections();
Spicetify.showNotification("Collection image set");
}
removeCollectionImage(uri) {
const collection = this.getCollection(uri);
if (!collection)
return;
collection.image = void 0;
this.saveCollections();
Spicetify.showNotification("Collection image removed");
}
};
var CollectionsWrapper2 = _CollectionsWrapper;
__publicField(CollectionsWrapper2, "INSTANCE", new _CollectionsWrapper());
window.CollectionsWrapper = CollectionsWrapper2.INSTANCE;
// src/extensions/folder_image_wrapper.ts
var _FolderImageWrapper = class extends EventTarget {
_folderImages;
constructor() {
super();
this._folderImages = JSON.parse(localStorage.getItem("library:folderImages") || "{}");
}
getFolderImage(uri) {
return this._folderImages[uri];
}
getFolderImages() {
return this._folderImages;
}
setFolderImage({ uri, url }) {
this._folderImages[uri] = url;
this.saveFolderImages();
Spicetify.showNotification("Folder image updated");
}
removeFolderImage(uri) {
delete this._folderImages[uri];
this.saveFolderImages();
Spicetify.showNotification("Folder image removed");
}
saveFolderImages() {
this.dispatchEvent(new CustomEvent("update", { detail: this._folderImages }));
localStorage.setItem("library:folderImages", JSON.stringify(this._folderImages));
}
};
var FolderImageWrapper2 = _FolderImageWrapper;
__publicField(FolderImageWrapper2, "INSTANCE", new _FolderImageWrapper());
window.FolderImageWrapper = FolderImageWrapper2.INSTANCE;
// src/extensions/extension.tsx
var styleLink = document.createElement("link");
styleLink.rel = "stylesheet";
styleLink.href = "/spicetify-routes-library.css";
document.head.appendChild(styleLink);
var setCardSize = (size) => {
document.documentElement.style.setProperty("--library-card-size", `${size}px`);
};
var setSearchBarSize = (enlarged) => {
const size = enlarged ? 300 : 200;
document.documentElement.style.setProperty("--library-searchbar-size", `${size}px`);
};
var FolderImage = ({ url }) => {
return /* @__PURE__ */ import_react10.default.createElement("img", {
alt: "Folder Image",
"aria-hidden": "true",
draggable: "false",
loading: "eager",
src: url,
className: "main-image-image x-entityImage-image main-image-loading main-image-loaded"
});
};
var SpicetifyLibrary = class {
ConfigWrapper = new config_wrapper_default(
[
{
name: "Card Size",
key: "cardSize",
type: "slider",
min: 100,
max: 200,
step: 0.05,
def: 180,
callback: setCardSize
},
{
name: "Extend Search Bar",
key: "extendSearchBar",
type: "toggle",
def: false,
callback: setSearchBarSize
},
{
name: "Add Local Albums Integration",
key: "localAlbums",
type: "toggle",
def: true,
desc: "You need to install the better-local-files app for this to work."
},
{
name: "Hide 'Your Library' Button",
key: "hideLibraryButton",
type: "toggle",
def: false,
desc: "This is experimental and may break the sidebar layout in some cases. Requires a spotify restart to take effect.",
sectionHeader: "Left Sidebar"
},
{
name: "Playlists Page",
key: "show-playlists",
type: "toggle",
def: true,
sectionHeader: "Pages"
},
{ name: "Albums Page", key: "show-albums", type: "toggle", def: true },
{ name: "Collections Page", key: "show-collections", type: "toggle", def: true },
{ name: "Artists Page", key: "show-artists", type: "toggle", def: true },
{ name: "Shows Page", key: "show-shows", type: "toggle", def: true }
],
"library"
);
};
window.SpicetifyLibrary = new SpicetifyLibrary();
(function wait() {
const { LocalStorageAPI } = Spicetify.Platform;
if (!LocalStorageAPI) {
setTimeout(wait, 100);
return;
}
main(LocalStorageAPI);
})();
function main(LocalStorageAPI) {
const isAlbum = (props) => props.uri?.includes("album") || props.id?.includes("local");
const isArtist = (props) => props.uri?.includes("artist");
Spicetify.ContextMenuV2.registerItem(/* @__PURE__ */ import_react10.default.createElement(album_menu_item_default, null), isAlbum);
Spicetify.ContextMenuV2.registerItem(/* @__PURE__ */ import_react10.default.createElement(artist_menu_item_default, null), isArtist);
Spicetify.Platform.LibraryAPI.getEvents()._emitter.addListener("update", () => CollectionsWrapper.cleanCollections());
function injectFolderImages() {
const rootlist = document.querySelector(".main-rootlist-wrapper > div:nth-child(2)");
if (!rootlist)
return setTimeout(injectFolderImages, 100);
setTimeout(() => {
for (const el of Array.from(rootlist.children)) {
const uri = el.querySelector("[aria-labelledby]")?.getAttribute("aria-labelledby")?.slice(14);
if (uri?.includes("folder")) {
const imageBox = el.querySelector(".x-entityImage-imageContainer");
if (!imageBox)
return;
const imageUrl = FolderImageWrapper.getFolderImage(uri);
if (imageUrl)
import_react_dom.default.render(/* @__PURE__ */ import_react10.default.createElement(FolderImage, {
url: imageUrl
}), imageBox);
}
}
}, 500);
}
injectFolderImages();
FolderImageWrapper.addEventListener("update", injectFolderImages);
function injectYLXButtons() {
const ylx_filter = document.querySelector(".main-yourLibraryX-libraryRootlist .main-yourLibraryX-libraryFilter");
if (!ylx_filter) {
return setTimeout(injectYLXButtons, 100);
}
injectFiltersButton(ylx_filter);
injectCollapseButton(ylx_filter);
}
function injectFiltersButton(ylx_filter) {
const toggleFiltersButton = document.createElement("span");
toggleFiltersButton.classList.add("toggle-filters-button");
ylx_filter.appendChild(toggleFiltersButton);
import_react_dom.default.render(/* @__PURE__ */ import_react10.default.createElement(toggle_filters_default, null), toggleFiltersButton);
}
function injectCollapseButton(ylx_filter) {
const collapseButton = document.createElement("span");
collapseButton.classList.add("collapse-button");
ylx_filter.appendChild(collapseButton);
import_react_dom.default.render(/* @__PURE__ */ import_react10.default.createElement(collapse_button_default, null), collapseButton);
}
if (!window.SpicetifyLibrary.ConfigWrapper.Config.hideLibraryButton) {
return;
}
document.body.classList.add("hide-library-button");
const state = LocalStorageAPI.getItem("left-sidebar-state");
if (state === 0)
injectYLXButtons();
LocalStorageAPI.getEvents()._emitter.addListener("update", (e) => {
const { key, value } = e.data;
if (key === "left-sidebar-state" && value === 0) {
injectFolderImages();
injectYLXButtons();
}
if (key === "left-sidebar-state" && value === 1) {
injectFolderImages();
}
});
}
})();
})();