diff --git a/globals.d.ts b/globals.d.ts index d6ce7dc8ea..56f7666700 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1581,91 +1581,6 @@ declare namespace Spicetify { */ onOutside?: (event: React.MouseEvent) => void; }; - type PanelSkeletonProps = { - /** - * Aria label for the panel. Does not set the panel header content. - */ - label?: string; - /** - * Item URI of the panel. Used as reference for Spotify's internal Event Factory. - * - * @deprecated Since Spotify `1.2.17` - */ - itemUri?: string; - /** - * Additional class name to apply to the panel. - * - * @deprecated Since Spotify `1.2.12` - */ - className?: string; - /** - * Additional styles to apply to the panel. - */ - style?: React.CSSProperties; - /** - * Children to render inside the panel. - */ - children?: React.ReactNode; - }; - type PanelContentProps = { - /** - * Additional class name to apply to the panel. - */ - className?: string; - /** - * Children to render inside the panel. - */ - children?: React.ReactNode; - }; - type PanelHeaderProps = { - /** - * Href for the header link. - * Can be either a URI for a path within the app, or a URL for an external link. - */ - link?: string; - /** - * Title of the header. - */ - title?: string; - /** - * Panel ID. Used to toggle panel open/closed state. - */ - panel: number; - /** - * Whether or not the panel contains advertisements. - * @default false - */ - isAdvert?: boolean; - /** - * Actions to render in the header. - */ - actions?: React.ReactNode | React.ReactNode[]; - /** - * Function to call when clicking on the close button. - * Called before the panel is closed. - */ - onClose?: () => void; - /** - * Prevent the panel from closing when clicking on the header close button. - * @default false - */ - preventDefaultClose?: boolean; - /** - * Function to call when clicking on the header back button. - * If not provided, the back button will not be rendered. - */ - onBack?: (event: React.MouseEvent) => void; - /** - * Font variant for the header title. - * @default "balladBold" - */ - titleVariant?: Variant; - /** - * Semantic color name for the header title. - * @default "textBase" - */ - titleSemanticColor?: SemanticColor; - }; type SliderProps = { /** * Label for the slider. @@ -1861,27 +1776,6 @@ declare namespace Spicetify { * @see Spicetify.ReactComponent.ConfirmDialogProps */ const ConfirmDialog: any; - /** - * Component to render Spotify-style panel skeleton - * - * Props: - * @see Spicetify.ReactComponent.PanelSkeletonProps - */ - const PanelSkeleton: any; - /** - * Component to render Spotify-style panel content - * - * Props: - * @see Spicetify.ReactComponent.PanelContentProps - */ - const PanelContent: any; - /** - * Component to render Spotify-style panel header - * - * Props: - * @see Spicetify.ReactComponent.PanelHeaderProps - */ - const PanelHeader: any; /** * Component to render Spotify slider * @@ -2203,13 +2097,6 @@ declare namespace Spicetify { dropOriginUri?: string ): (event: React.DragEvent, uris?: string[], label?: string, contextUri?: string, sectionIndex?: number) => void; - /** - * React Hook to use panel state - * @param id ID of the panel to use - * @return Object with methods of the panel - */ - function usePanelState(id: number): { toggle: () => void; isActive: boolean }; - /** * React Hook to use extracted color from GraphQL * @@ -2227,180 +2114,6 @@ declare namespace Spicetify { function useExtractedColor(uri: string, fallbackColor?: string, variant?: "colorRaw" | "colorLight" | "colorDark"): string; } - /** - * An API wrapper to interact with Spotify's Panel/right sidebar. - */ - namespace Panel { - /** - * Properties that are used by the `registerPanel` function. - */ - type PanelProps = { - /** - * Label of the Panel. - */ - label?: string; - /** - * Children to render inside the Panel. - * Must be a React Component. - * Will be passed a `panel` prop with the Panel ID. - */ - children: React.ReactNode; - /** - * Determine if the children passed is a custom Panel. - * If true, the children will be rendered as is. - * Note: All passed props except `children` will be ignored if enabled. - * - * @default false - */ - isCustom?: boolean; - /** - * Inline styles to apply to the Panel skeleton. - */ - style?: React.CSSProperties; - /** - * Additional class name to apply to the Panel content wrapper. - */ - wrapperClassname?: string; - /** - * Additional class name to apply to the Panel header. - */ - headerClassname?: string; - /** - * Font variant for the Panel header title. - * @default "balladBold" - */ - headerVariant?: Variant; - /** - * Semantic color name for the Panel header title. - * @default "textBase" - */ - headerSemanticColor?: SemanticColor; - /** - * Href for the header link. - * Can be either a URI for a path within the app, or a URL for an external link. - */ - headerLink?: string; - /** - * Additional actions to render in the header. - * Will be rendered next to the close button. - */ - headerActions?: React.ReactNode | React.ReactNode[]; - /** - * Function to call when clicking on the header close button. - * Called before the panel is closed. - */ - headerOnClose?: () => void; - /** - * Prevent the panel from closing when clicking on the header close button. - * @default false - */ - headerPreventDefaultClose?: boolean; - /** - * Function to call when clicking on the header back button. - * If not provided, the back button will not be rendered. - * @param event Event object - */ - headerOnBack?: (event: React.MouseEvent) => void; - }; - - /** - * An object of reserved panel IDs used by Spotify. - */ - const reservedPanelIds: Record; - /** - * Collection of React Components used by Spotify in the Panel. - */ - const Components: { - /** - * React Component for the Panel's skeleton. - * - * Props: - * @see Spicetify.ReactComponent.PanelSkeletonProps - */ - PanelSkeleton: any; - /** - * React Component for the Panel's content. - * - * Props: - * @see Spicetify.ReactComponent.PanelContentProps - */ - PanelContent: any; - /** - * React Component for the Panel's header. - * - * Props: - * @see Spicetify.ReactComponent.PanelHeaderProps - */ - PanelHeader: any; - }; - /** - * Check whether or not a Panel with the provided ID is registered. - * @param id Panel ID to check - * @return Whether or not a Panel with the provided ID is registered - */ - function hasPanel(id: number): boolean; - /** - * Get the Panel with the provided ID. - * @param id Panel ID to get - * @return Panel with the provided ID - */ - function getPanel(id: number): React.ReactNode | undefined; - /** - * Set the Panel with the provided ID. - * If the ID is not registered, it will be set to `0`. - * @param id Panel ID to set - */ - function setPanel(id: number): Promise; - /** - * Subscribe to Panel changes. - * @param callback Callback to call when Panel changes - */ - function subPanelState(callback: (id: number) => void): void; - /** - * Register a new Panel. - * An ID will be automatically assigned to the Panel. - * - * To make it easier and convenient for developers to use the Panel API, this method by default wraps the children passed into a Panel skeleton and content wrapper. - * - * If you wish to customize the Panel, you can pass `isCustom` as `true` to disable the default wrapper. - * - * @param props Properties of the Panel - * @return Methods and properties of the Panel - */ - function registerPanel(props: PanelProps): { - /** - * Assigned ID of the Panel. - */ - id: number; - /** - * Function to toggle the Panel open/closed state. - */ - toggle: () => Promise; - /** - * Method to subscribe to the related Panel state. - * Only fires when the related Panel open/closed state changes. - */ - onStateChange: (callback: (isActive: boolean) => void) => void; - /** - * Boolean to determine if the Panel is open. - */ - isActive: boolean; - }; - /** - * Function to render a Panel of the current ID. - * If the ID is not registered or is reserved by Spotify, the function will return `null`. - * - * Used as a hook for Spotify internal component. - * @return Panel of the current ID - */ - function render(): React.ReactNode | null; - /** - * ID of the current Panel. - * @return ID of the current Panel - */ - const currentPanel: number; - } - /** * react-flip-toolkit * @description A lightweight magic-move library for configurable layout transitions. diff --git a/jsHelper/spicetifyWrapper.js b/jsHelper/spicetifyWrapper.js index 4d47811bd8..460ae11dc9 100644 --- a/jsHelper/spicetifyWrapper.js +++ b/jsHelper/spicetifyWrapper.js @@ -188,14 +188,15 @@ window.Spicetify = { "_sidebarXItemToClone", "AppTitle", "_reservedPanelIds", - "Panel", "ReactFlipToolkit", "classnames", "ReactQuery", "Color", "extractColorPreset", "ReactDOMServer", - "Snackbar" + "Snackbar", + "ContextMenuV2", + "ReactJSX" ]) }, { @@ -257,9 +258,6 @@ window.Spicetify = { "TextComponent", "IconComponent", "ConfirmDialog", - "PanelContent", - "PanelSkeleton", - "PanelHeader", "Slider", "RemoteConfigProvider", "ButtonPrimary", @@ -274,7 +272,8 @@ window.Spicetify = { "Route", "StoreProvider", "PlatformProvider", - "Dropdown" + "Dropdown", + "MenuSubMenuItem" ]) }, { @@ -285,7 +284,7 @@ window.Spicetify = { { objectToCheck: Spicetify.ReactHook, name: "Spicetify.ReactHook", - methods: new Set(["DragHandler", "usePanelState", "useExtractedColor"]) + methods: new Set(["DragHandler", "useExtractedColor"]) } ]); @@ -309,7 +308,7 @@ window.Spicetify = { return; } - const corsProxyURL = "https://cors-proxy.spicetify.app/"; + const corsProxyURL = "https://cors-proxy.spicetify.app"; const allowedMethodsMap = { get: "get", post: "post", @@ -321,21 +320,18 @@ window.Spicetify = { const internalEndpoints = new Set(["sp:", "wg:"]); const handler = { - // biome-ignore lint/complexity/useArrowFunction: - get: function (target, prop, receiver) { + get: (target, prop, receiver) => { const internalFetch = Reflect.get(target, prop, receiver); if (typeof internalFetch !== "function" || !allowedMethodsSet.has(prop) || Spicetify.Platform.version < "1.2.31") return internalFetch; - // biome-ignore lint/complexity/useArrowFunction: return async function (url, body) { const urlObj = new URL(url); const isWebAPI = urlObj.hostname === "api.spotify.com"; const isSpClientAPI = urlObj.hostname.includes("spotify.com") && urlObj.hostname.includes("spclient"); const isInternalURL = internalEndpoints.has(urlObj.protocol); - // biome-ignore lint/style/noArguments: - if (isInternalURL) return internalFetch.apply(this, arguments); + if (isInternalURL) return internalFetch.apply(this, [url, body]); const shouldUseCORSProxy = !isWebAPI && !isSpClientAPI && !isInternalURL; @@ -366,7 +362,7 @@ window.Spicetify = { finalURL += `?${params.toString()}`; } else options.body = isJson ? JSON.stringify(body) : body; } - if (shouldUseCORSProxy) finalURL = `${corsProxyURL}${finalURL}`; + if (shouldUseCORSProxy) finalURL = `${corsProxyURL}/${finalURL}`; const Authorization = `Bearer ${Spicetify.Platform.AuthorizationAPI.getState().token.accessToken}`; let injectedHeaders = {}; @@ -413,12 +409,12 @@ window.Spicetify = { })(); (function hotloadWebpackModules() { - if (!window?.webpackChunkopen) { + if (!window?.webpackChunkclient_web) { setTimeout(hotloadWebpackModules, 50); return; } // Force all webpack modules to load - const require = webpackChunkopen.push([[Symbol()], {}, re => re]); + const require = webpackChunkclient_web.push([[Symbol()], {}, re => re]); const chunks = require.m ? Object.entries(require.m) : []; if (!chunks) { setTimeout(hotloadWebpackModules, 50); @@ -534,13 +530,6 @@ window.Spicetify = { m.toString().includes("action") && m.toString().includes("open") && m.toString().includes("trigger") && m.toString().includes("right-click") ), TooltipWrapper: functionModules.find(m => m.toString().includes("renderInline") && m.toString().includes("showDelay")), - PanelHeader: functionModules.find(m => m.toString().includes("panel") && m.toString().includes("actions")), - PanelContent: - modules.find(m => m?.render?.toString().includes("scrollBarContainer")) || - functionModules.find(m => m.toString().includes("scrollBarContainer")), - PanelSkeleton: - functionModules.find(m => m.toString().includes("label") && m.toString().includes("aside")) || - modules.find(m => m?.render?.toString().includes('"section"')), ButtonPrimary: modules.find(m => m?.render && m?.displayName === "ButtonPrimary"), ButtonSecondary: modules.find(m => m?.render && m?.displayName === "ButtonSecondary"), ButtonTertiary: modules.find(m => m?.render && m?.displayName === "ButtonTertiary"), @@ -566,8 +555,6 @@ window.Spicetify = { }, ReactHook: { DragHandler: functionModules.find(m => m.toString().includes("dataTransfer") && m.toString().includes("data-dragging")), - // deprecated since 1.2.17 - usePanelState: functionModules.find(m => m.toString().includes("setPanelState")), useExtractedColor: functionModules.find( m => m.toString().includes("extracted-color") || (m.toString().includes("colorRaw") && m.toString().includes("useEffect")) ) @@ -2287,179 +2274,6 @@ Spicetify.Playbar = (() => { return { Button, Widget }; })(); -// TODO: Remove this with v3 release -(function waitForPanelAPI() { - if (!Spicetify.Platform?.PanelAPI || !Spicetify.React || !Spicetify._reservedPanelIds) { - setTimeout(waitForPanelAPI, 300); - return; - } - - // Workaround for older versions - let currentPanelId = 0; - let fallback = false; - let refreshTimeout; - let init = true; - - if (!Spicetify.Platform.PanelAPI.getLastCachedPanelState) { - fallback = true; - Spicetify.Platform.PanelAPI.subscribeToPanelState(panelId => { - currentPanelId = panelId; - }); - } - - const contentMap = new Map( - Object.entries(Spicetify._reservedPanelIds) - .map(([key, value]) => !Number.isNaN(parseInt(key)) && [parseInt(key), value]) - .filter(Boolean) - ); - - // https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary - class ErrorBoundary extends Spicetify.React.Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - // Update state so the next render will show the fallback UI. - return { hasError: true }; - } - - componentDidCatch(error, info) { - const extension = - Spicetify.Config.extensions.find(ext => error.stack.includes(ext)) || - Spicetify.Config.custom_apps.find(app => error.stack.includes(`spicetify-routes-${app}`)); - - Spicetify.showNotification( - `Something went wrong in panel ID "${this.props.id}" ${extension ? `of "${extension}"` : ""}, check Console for error log`, - true - ); - console.error(error); - console.error(`Error stack in panel ID "${this.props.id}": ${info.componentStack}`); - Spicetify.Panel.setPanel(Spicetify.Panel.reservedPanelIds.Disabled); - } - - render() { - if (this.state.hasError) { - // `false` not `null`, so it won’t render beyond the null coalescing operator. - return false; - } - - // Pass the `panel` prop with the current panel ID to the children - return Spicetify.React.cloneElement(this.props.children, { panel: this.props.id }); - } - } - - Spicetify.Panel = { - reservedPanelIds: Spicetify._reservedPanelIds, - Components: { - PanelSkeleton: Spicetify.ReactComponent.PanelSkeleton, - PanelContent: Spicetify.ReactComponent.PanelContent, - PanelHeader: Spicetify.ReactComponent.PanelHeader - }, - hasPanel: (id, _internal) => { - // Render is sometimes ran before the wrapper is initialized, so we need to refresh it - // For some reason it doesn't trigger listeners - if (_internal && init) { - clearTimeout(refreshTimeout); - refreshTimeout = setTimeout(() => { - Spicetify.Panel.setPanel(id); - init = false; - }, 100); - } - return contentMap.has(id); - }, - getPanel: id => contentMap.get(id), - render: () => { - const { currentPanel } = Spicetify.Panel; - return (!Spicetify.Panel.reservedPanelIds[currentPanel] && contentMap.get(Spicetify.Panel.currentPanel)) || null; - }, - get currentPanel() { - return fallback ? currentPanelId : Spicetify.Platform.PanelAPI.getLastCachedPanelState(); - }, - setPanel: async id => { - currentPanelId = id; - await Spicetify.Platform.PanelAPI.setPanelState(id); - }, - subPanelState: callback => Spicetify.Platform.PanelAPI.subscribeToPanelState(callback), - registerPanel: ({ - label, - children, - isCustom = false, - style, - wrapperClassname, - headerClassname, - headerVariant, - headerSemanticColor, - headerLink, - headerActions, - headerOnClose, - headerPreventDefaultClose, - headerOnBack - }) => { - const id = [...contentMap.keys()].sort((a, b) => a - b).pop() + 1; - const content = isCustom - ? children - : Spicetify.React.createElement( - Spicetify.ReactComponent.PanelSkeleton, - { - label, - // Backwards compatibility, no longer needed in Spotify 1.2.12 - className: "Root__right-sidebar", - style - }, - Spicetify.React.createElement( - Spicetify.ReactComponent.PanelContent, - { - className: wrapperClassname - }, - Spicetify.React.createElement(Spicetify.ReactComponent.PanelHeader, { - title: label, - panel: id, - link: headerLink, - actions: headerActions, - onClose: headerOnClose, - onBack: headerOnBack, - preventDefaultClose: headerPreventDefaultClose, - className: headerClassname, - titleVariant: headerVariant, - titleSemanticColor: headerSemanticColor - }), - Spicetify.React.cloneElement(children, { panel: id }) - ) - ); - - contentMap.set(id, Spicetify.React.createElement(ErrorBoundary, { id }, content)); - - let isActive = Spicetify.Panel.currentPanel === id; - - return { - id, - toggle: async () => { - const { currentPanel } = Spicetify.Panel; - currentPanelId = currentPanel === id ? 0 : id; - await Spicetify.Panel.setPanel(currentPanel === id ? 0 : id); - }, - onStateChange: callback => { - Spicetify.Panel.subPanelState(panel => { - const activeState = panel === id; - if (activeState !== isActive) { - isActive = activeState; - callback(isActive); - } - }); - }, - get isActive() { - return Spicetify.Panel.currentPanel === id; - } - }; - } - }; - - // Eliminate false positives - Spicetify.Panel.subPanelState(() => clearTimeout(refreshTimeout)); -})(); - (async function checkForUpdate() { if (!Spicetify.Config) { setTimeout(checkForUpdate, 300); diff --git a/src/apply/apply.go b/src/apply/apply.go index 553f54feed..71ade3945c 100644 --- a/src/apply/apply.go +++ b/src/apply/apply.go @@ -96,30 +96,30 @@ func htmlMod(htmlPath string, flags Flag) { helperHTML := "\n" if flags.InjectThemeJS { - extensionsHTML += `` + "\n" + extensionsHTML += "\n" } if flags.SidebarConfig { - helperHTML += `` + "\n" + helperHTML += "\n" } if flags.HomeConfig { - helperHTML += `` + "\n" + helperHTML += "\n" } if flags.ExpFeatures { - helperHTML += `` + "\n" + helperHTML += "\n" } if flags.SpicetifyVer != "" { var extList string for _, ext := range flags.Extension { - extList += `"` + ext + `",` + extList += fmt.Sprintf(`"%s",`, ext) } var customAppList string for _, app := range flags.CustomApp { - customAppList += `"` + app + `",` + customAppList += fmt.Sprintf(`"%s",`, app) } helperHTML += fmt.Sprintf(`` + "\n" + extensionsHTML += fmt.Sprintf("\n", v) } else { - extensionsHTML += `` + "\n" + extensionsHTML += fmt.Sprintf("\n", v) } } @@ -147,9 +147,9 @@ func htmlMod(htmlPath string, flags Flag) { if err == nil { for _, extensionFile := range manifest.ExtensionFiles { if strings.HasSuffix(extensionFile, ".mjs") { - extensionsHTML += `` + "\n" + extensionsHTML += fmt.Sprintf("\n", v, extensionFile) } else { - extensionsHTML += `` + "\n" + extensionsHTML += fmt.Sprintf("\n", v, extensionFile) } } } @@ -159,11 +159,15 @@ func htmlMod(htmlPath string, flags Flag) { utils.Replace( &content, `<\!-- spicetify helpers -->`, - "${0}"+helperHTML) + func(submatches ...string) string { + return fmt.Sprintf("%s%s", submatches[0], helperHTML) + }) utils.Replace( &content, ``, - extensionsHTML+"${0}") + func(submatches ...string) string { + return fmt.Sprintf("%s%s", extensionsHTML, submatches[0]) + }) return content }) } @@ -214,8 +218,8 @@ func getColorCSS(scheme map[string]string) string { func insertCustomApp(jsPath string, flags Flag) { utils.ModifyFile(jsPath, func(content string) string { - const REACT_REGEX = `(\w+(?:\(\))?)\.lazy\(\((?:\(\)=>|function\(\)\{return )(\w+)\.(\w+)\(\d+\)\.then\(\w+\.bind\(\w+,\d+\)\)\}?\)\)` - const REACT_ELEMENT_REGEX = `(\w+(?:\(\))?\.createElement|\([\w$\.,]+\))\(([\w\.]+),\{path:"\/collection"(?:,(element|children)?[:.\w,{}()/*"]+)?\}` + const REACT_REGEX = `([\w_\$][\w_\$\d]*(?:\(\))?)\.lazy\(\((?:\(\)=>|function\(\)\{return )(\w+)\.(\w+)\(\d+\)\.then\(\w+\.bind\(\w+,\d+\)\)\}?\)\)` + const REACT_ELEMENT_REGEX = `(\[\w_\$][\w_\$\d]*(?:\(\))?\.createElement|\([\w$\.,]+\))\(([\w\.]+),\{path:"\/collection"(?:,(element|children)?[:.\w,{}()/*"]+)?\}` reactSymbs := utils.FindSymbol( "Custom app React symbols", content, @@ -266,32 +270,44 @@ func insertCustomApp(jsPath string, flags Flag) { utils.Replace( &content, `\{(\d+:"xpui)`, - `{`+appMap+`${1}`) + func(submatches ...string) string { + return fmt.Sprintf("{%s%s", appMap, submatches[1]) + }) utils.ReplaceOnce( &content, REACT_REGEX, - `${0}`+appReactMap) + func(submatches ...string) string { + return fmt.Sprintf("%s%s", submatches[0], appReactMap) + }) utils.ReplaceOnce( &content, REACT_ELEMENT_REGEX, - appEleMap+`${0}`) + func(submatches ...string) string { + return fmt.Sprintf("%s%s", appEleMap, submatches[0]) + }) utils.Replace( &content, `(?:\w+(?:\(\))?\.createElement|\([\w$\.,]+\))\("li",\{className:[\w$\.]+\}?,(?:children:)?[\w$\.,()]+\(\w+,\{uri:"spotify:user:@:collection",to:"/collection"`, - `Spicetify._sidebarItemToClone=${0}`) + func(submatches ...string) string { + return fmt.Sprintf("Spicetify._sidebarItemToClone=%s", submatches[0]) + }) utils.Replace( &content, `(?:\w+(?:\(\))?\.createElement|\([\w$.,_]+\))\("li",{className:[-\w".${}()?!:, ]+,children:(?:\w+(?:\(\))?\.createElement|\([\w$.,_]+\))\([\w$._]+,{label:[-\w".${}()?!:, ]+,(\w+:[-\w".${}()?!&: ]+,)*children:(?:\w+(?:\(\))?\.createElement|\([\w$.,_]+\))\([\w$._]+,\{to:"/search"`, - `Spicetify._sidebarXItemToClone=${0}`) + func(submatches ...string) string { + return fmt.Sprintf("Spicetify._sidebarXItemToClone=%s", submatches[0]) + }) utils.ReplaceOnce( &content, `\d+:1,\d+:1,\d+:1`, - "${0}"+cssEnableMap) + func(submatches ...string) string { + return fmt.Sprintf("%s%s", submatches[0], cssEnableMap) + }) sidebarItemMatch := utils.SeekToCloseParen( content, @@ -327,14 +343,18 @@ func insertCustomApp(jsPath string, flags Flag) { utils.ReplaceOnce( &content, `return null!=\w+&&\w+\.totalLength(\?\w+\(\)\.createElement\(\w+,\{contextUri:)(\w+)\.uri`, - `return true${1}${2}?.uri||""`) + func(submatches ...string) string { + return fmt.Sprintf(`return true%s%s?.uri||""`, submatches[1], submatches[2]) + }) } if flags.ExpFeatures { utils.ReplaceOnce( &content, `(([\w$.]+\.fromJSON)\(\w+\)+;)(return ?[\w{}().,]+[\w$]+\.Provider,)(\{value:\{localConfiguration)`, - `${1}Spicetify.createInternalMap=${2};${3}Spicetify.RemoteConfigResolver=${4}`) + func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.createInternalMap=%s;%sSpicetify.RemoteConfigResolver=%s", submatches[1], submatches[2], submatches[3], submatches[4]) + }) } return content @@ -350,7 +370,9 @@ func insertHomeConfig(jsPath string, flags Flag) { utils.ReplaceOnce( &content, `([\w$_\.]+\.sections\.items)(\.map)`, - `SpicetifyHomeConfig.arrange(${1})${2}`) + func(submatches ...string) string { + return fmt.Sprintf("SpicetifyHomeConfig.arrange(%s)%s", submatches[1], submatches[2]) + }) return content }) } @@ -374,7 +396,9 @@ func insertExpFeatures(jsPath string, flags Flag) { utils.ReplaceOnce( &content, `(function \w+\((\w+)\)\{)(\w+ \w+=\w\.name;if\("internal")`, - `${1}${2}=Spicetify.expFeatureOverride(${2});${3}`) + func(submatches ...string) string { + return fmt.Sprintf("%s%s=Spicetify.expFeatureOverride(%s);%s", submatches[1], submatches[2], submatches[2], submatches[3]) + }) return content }) } @@ -384,12 +408,14 @@ func insertVersionInfo(jsPath string, flags Flag) { utils.ReplaceOnce( &content, `(\w+(?:\(\))?\.createElement|\([\w$\.,]+\))\([\w\."]+,[\w{}():,]+\.containerVersion\}?\),`, - `${0}${1}("details",{children: [ - ${1}("summary",{children: "Spicetify v" + Spicetify.Config.version}), - ${1}("li",{children: "Theme: " + Spicetify.Config.current_theme + (Spicetify.Config.color_scheme && " / ") + Spicetify.Config.color_scheme}), - ${1}("li",{children: "Extensions: " + Spicetify.Config.extensions.join(", ")}), - ${1}("li",{children: "Custom apps: " + Spicetify.Config.custom_apps.join(", ")}), - ]}),`) + func(submatches ...string) string { + return fmt.Sprintf(`%s%s("details",{children: [ + %s("summary",{children: "Spicetify v" + Spicetify.Config.version}), + %s("li",{children: "Theme: " + Spicetify.Config.current_theme + (Spicetify.Config.color_scheme && " / ") + Spicetify.Config.color_scheme}), + %s("li",{children: "Extensions: " + Spicetify.Config.extensions.join(", ")}), + %s("li",{children: "Custom apps: " + Spicetify.Config.custom_apps.join(", ")}), + ]}),`, submatches[0], submatches[1], submatches[1], submatches[1], submatches[1], submatches[1]) + }) return content }) } diff --git a/src/cmd/apply.go b/src/cmd/apply.go index cfbf6d771a..628fb3b694 100644 --- a/src/cmd/apply.go +++ b/src/cmd/apply.go @@ -328,7 +328,7 @@ func RefreshApps(list ...string) { continue } if len(assetsList) == 0 { - message := fmt.Sprintf("Custom App '%s': no assets found for expression \"%s\"", app, assetExpr) + message := fmt.Sprintf("Custom App '%s': no assets found for expression '%s'", app, assetExpr) utils.PrintWarning(message) continue } @@ -359,7 +359,7 @@ func RefreshApps(list ...string) { } jsTemplate := fmt.Sprintf( - `(("undefined"!=typeof self?self:global).webpackChunkopen=("undefined"!=typeof self?self:global).webpackChunkopen||[]) + `(("undefined"!=typeof self?self:global).webpackChunkclient_web=("undefined"!=typeof self?self:global).webpackChunkclient_web||[]) .push([["%s"],{"%s":(e,t,n)=>{ "use strict";n.r(t),n.d(t,{default:()=>render}); %s diff --git a/src/preprocess/preprocess.go b/src/preprocess/preprocess.go index a24d6acf75..b9b2d6dee8 100644 --- a/src/preprocess/preprocess.go +++ b/src/preprocess/preprocess.go @@ -106,31 +106,33 @@ func Start(version string, extractedAppsPath string, flags Flag) { } } for k, v := range cssTranslationMap { - utils.Replace(&content, k, v) + utils.Replace(&content, k, func(submatches ...string) string { + return v + }) } content = colorVariableReplaceForJS(content) - // Webpack name changed from v1.1.72 - utils.Replace(&content, "webpackChunkclient_web", "webpackChunkopen") return content }) case ".css": utils.ModifyFile(path, func(content string) string { for k, v := range cssTranslationMap { - utils.Replace(&content, k, v) + utils.Replace(&content, k, func(submatches ...string) string { + return v + }) } if flags.RemoveRTL { content = removeRTL(content) } // Temporary fix for top bar opacity bug if fileName == "xpui.css" { - content = content + ` -.main-topBar-topbarContent:not(.main-topBar-topbarContentFadeIn)>* { - opacity: unset !important; -} -.main-entityHeader-topbarContent:not(.main-entityHeader-topbarContentFadeIn)>* { - opacity: 0 !important; -}` + content = fmt.Sprintf(`%s + .main-topBar-topbarContent:not(.main-topBar-topbarContentFadeIn)>* { + opacity: unset !important; + } + .main-entityHeader-topbarContent:not(.main-entityHeader-topbarContentFadeIn)>* { + opacity: 0 !important; + }`, content) } return content }) @@ -138,23 +140,23 @@ func Start(version string, extractedAppsPath string, flags Flag) { case ".html": utils.ModifyFile(path, func(content string) string { var tags string - tags += `` + "\n" - tags += `` + "\n" + tags += "\n" + tags += "\n" if flags.ExposeAPIs { - tags += `` + "\n" - tags += `` + "\n" + tags += "\n" + tags += "\n" } - utils.Replace(&content, ``, "${0}\n"+tags) + utils.Replace(&content, ``, func(submatches ...string) string { + return fmt.Sprintf("%s\n%s", submatches[0], tags) + }) return content }) } return nil }) - - fakeZLink(filepath.Join(extractedAppsPath, "zlink")) } // StartCSS modifies all CSS files in extractedAppsPath to change @@ -170,117 +172,265 @@ func StartCSS(extractedAppsPath string) { } func colorVariableReplace(content string) string { - utils.Replace(&content, "#181818", "var(--spice-player)") - utils.Replace(&content, "#212121", "var(--spice-player)") + utils.Replace(&content, "#181818", func(submatches ...string) string { + return "var(--spice-player)" + }) + utils.Replace(&content, "#212121", func(submatches ...string) string { + return "var(--spice-player)" + }) - utils.Replace(&content, "#282828", "var(--spice-card)") + utils.Replace(&content, "#282828", func(submatches ...string) string { + return "var(--spice-card)" + }) - utils.Replace(&content, "#121212", "var(--spice-main)") - utils.Replace(&content, "#242424", "var(--spice-main-elevated)") + utils.Replace(&content, "#121212", func(submatches ...string) string { + return "var(--spice-main)" + }) + utils.Replace(&content, "#242424", func(submatches ...string) string { + return "var(--spice-main-elevated)" + }) - utils.Replace(&content, "#1a1a1a", "var(--spice-highlight)") - utils.Replace(&content, "#2a2a2a", "var(--spice-highlight-elevated)") + utils.Replace(&content, "#1a1a1a", func(submatches ...string) string { + return "var(--spice-highlight)" + }) + utils.Replace(&content, "#2a2a2a", func(submatches ...string) string { + return "var(--spice-highlight-elevated)" + }) - utils.Replace(&content, "#000", "var(--spice-sidebar)") - utils.Replace(&content, "#000000", "var(--spice-sidebar)") + utils.Replace(&content, "#000", func(submatches ...string) string { + return "var(--spice-sidebar)" + }) + utils.Replace(&content, "#000000", func(submatches ...string) string { + return "var(--spice-sidebar)" + }) - utils.Replace(&content, "white;", " var(--spice-text);") - utils.Replace(&content, "#fff", "var(--spice-text)") - utils.Replace(&content, "#ffffff", "var(--spice-text)") - utils.Replace(&content, "#f8f8f8", " var(--spice-text)") + utils.Replace(&content, "white;", func(submatches ...string) string { + return " var(--spice-text);" + }) + utils.Replace(&content, "#fff", func(submatches ...string) string { + return "var(--spice-text)" + }) + utils.Replace(&content, "#ffffff", func(submatches ...string) string { + return "var(--spice-text)" + }) + utils.Replace(&content, "#f8f8f8", func(submatches ...string) string { + return "var(--spice-text)" + }) - utils.Replace(&content, "#b3b3b3", "var(--spice-subtext)") - utils.Replace(&content, "#a7a7a7", "var(--spice-subtext)") + utils.Replace(&content, "#b3b3b3", func(submatches ...string) string { + return "var(--spice-subtext)" + }) + utils.Replace(&content, "#a7a7a7", func(submatches ...string) string { + return "var(--spice-subtext)" + }) - utils.Replace(&content, "#1db954", "var(--spice-button)") - utils.Replace(&content, "#1877f2", "var(--spice-button)") + utils.Replace(&content, "#1db954", func(submatches ...string) string { + return "var(--spice-button)" + }) + utils.Replace(&content, "#1877f2", func(submatches ...string) string { + return "var(--spice-button)" + }) - utils.Replace(&content, "#1ed760", "var(--spice-button-active)") - utils.Replace(&content, "#1fdf64", "var(--spice-button-active)") - utils.Replace(&content, "#169c46", "var(--spice-button-active)") + utils.Replace(&content, "#1ed760", func(submatches ...string) string { + return "var(--spice-button-active)" + }) + utils.Replace(&content, "#1fdf64", func(submatches ...string) string { + return "var(--spice-button-active)" + }) + utils.Replace(&content, "#169c46", func(submatches ...string) string { + return "var(--spice-button-active)" + }) - utils.Replace(&content, "#535353", "var(--spice-button-disabled)") + utils.Replace(&content, "#535353", func(submatches ...string) string { + return "var(--spice-button-disabled)" + }) - utils.Replace(&content, "#333", "var(--spice-tab-active)") - utils.Replace(&content, "#333333", "var(--spice-tab-active)") + utils.Replace(&content, "#333", func(submatches ...string) string { + return "var(--spice-tab-active)" + }) + utils.Replace(&content, "#333333", func(submatches ...string) string { + return "var(--spice-tab-active)" + }) - utils.Replace(&content, "#7f7f7f", "var(--spice-misc)") + utils.Replace(&content, "#7f7f7f", func(submatches ...string) string { + return "var(--spice-misc)" + }) - utils.Replace(&content, "#4687d6", "var(--spice-notification)") - utils.Replace(&content, "#2e77d0", "var(--spice-notification)") + utils.Replace(&content, "#4687d6", func(submatches ...string) string { + return "var(--spice-notification)" + }) + utils.Replace(&content, "#2e77d0", func(submatches ...string) string { + return "var(--spice-notification)" + }) - utils.Replace(&content, "#e22134", "var(--spice-notification-error)") - utils.Replace(&content, "#cd1a2b", "var(--spice-notification-error)") + utils.Replace(&content, "#e22134", func(submatches ...string) string { + return "var(--spice-notification-error)" + }) + utils.Replace(&content, "#cd1a2b", func(submatches ...string) string { + return "var(--spice-notification-error)" + }) - utils.Replace(&content, `rgba\(18,18,18,([\d\.]+)\)`, "rgba(var(--spice-rgb-main),${1})") - utils.Replace(&content, `rgba\(40,40,40,([\d\.]+)\)`, "rgba(var(--spice-rgb-card),${1})") - utils.Replace(&content, `rgba\(0,0,0,([\d\.]+)\)`, "rgba(var(--spice-rgb-shadow),${1})") - utils.Replace(&content, `hsla\(0,0%,100%,\.9\)`, "rgba(var(--spice-rgb-text),.9)") - utils.Replace(&content, `hsla\(0,0%,100%,([\d\.]+)\)`, "rgba(var(--spice-rgb-selected-row),${1})") + utils.Replace(&content, `rgba\(18,18,18,([\d\.]+)\)`, func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-main),%s)", submatches[1]) + }) + utils.Replace(&content, `rgba\(40,40,40,([\d\.]+)\)`, func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-card),%s)", submatches[1]) + }) + utils.Replace(&content, `rgba\(0,0,0,([\d\.]+)\)`, func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-rgb-shadow),%s)", submatches[1]) + }) + utils.Replace(&content, `hsla\(0,0%,100%,\.9\)`, func(submatches ...string) string { + return "rgba(var(--spice-rgb-text),.9)" + }) + utils.Replace(&content, `hsla\(0,0%,100%,([\d\.]+)\)`, func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-rgb-selected-row),%s)", submatches[1]) + }) return content } func colorVariableReplaceForJS(content string) string { - utils.Replace(&content, `"#1db954"`, ` getComputedStyle(document.body).getPropertyValue("--spice-button").trim()`) - utils.Replace(&content, `"#b3b3b3"`, ` getComputedStyle(document.body).getPropertyValue("--spice-subtext").trim()`) - utils.Replace(&content, `"#ffffff"`, ` getComputedStyle(document.body).getPropertyValue("--spice-text").trim()`) - utils.Replace(&content, `color:"white"`, `color:"var(--spice-text)"`) + utils.Replace(&content, `"#1db954"`, func(submatches ...string) string { + return ` getComputedStyle(document.body).getPropertyValue("--spice-button").trim()` + }) + utils.Replace(&content, `"#b3b3b3"`, func(submatches ...string) string { + return ` getComputedStyle(document.body).getPropertyValue("--spice-subtext").trim()` + }) + utils.Replace(&content, `"#ffffff"`, func(submatches ...string) string { + return ` getComputedStyle(document.body).getPropertyValue("--spice-text").trim()` + }) + utils.Replace(&content, `color:"white"`, func(submatches ...string) string { + return `color:"var(--spice-text)"` + }) return content } func disableSentry(input string) string { - utils.Replace(&input, `(?:prototype\.)?bindClient(?:=function)?\(\w+\)\{`, "${0}return;") + utils.Replace(&input, `(?:prototype\.)?bindClient(?:=function)?\(\w+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) return input } func disableLogging(input string) string { - utils.Replace(&input, `sp://logging/v3/\w+`, "") - utils.Replace(&input, `[^"\/]+\/[^"\/]+\/(public\/)?v3\/events`, "") - - utils.Replace(&input, `key:"registerEventListeners",value:function\(\)\{`, "${0}return;") - utils.Replace(&input, `key:"logInteraction",value:function\([\w,]+\)\{`, "${0}return{interactionId:null,pageInstanceId:null};") - utils.Replace(&input, `key:"logNonAuthInteraction",value:function\([\w,]+\)\{`, "${0}return{interactionId:null,pageInstanceId:null};") - utils.Replace(&input, `key:"logImpression",value:function\([\w,]+\)\{`, "${0}return;") - utils.Replace(&input, `key:"logNonAuthImpression",value:function\([\w,]+\)\{`, "${0}return;") - utils.Replace(&input, `key:"logNavigation",value:function\([\w,]+\)\{`, "${0}return;") - utils.Replace(&input, `key:"handleBackgroundStates",value:function\(\)\{`, "${0}return;") - utils.Replace(&input, `key:"createLoggingParams",value:function\([\w,]+\)\{`, "${0}return;") - utils.Replace(&input, `key:"initSendingEvents",value:function\(\)\{`, "${0}return;") - utils.Replace(&input, `key:"flush",value:function\(\)\{`, "${0}return;") - utils.Replace(&input, `(\{key:"send",value:function\([\w,]+\))\{[\d\w\s,{}()[\]\.,!\?=>&|;:_""]+?\}(\},\{key:"hasContext")`, "${1}{return;}${2}") - utils.Replace(&input, `key:"lastFlush",value:function\(\)\{`, "${0}return;") - - utils.Replace(&input, `(\}registerEventListeners\(\))\{.+?\}(unregisterEventListeners)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}logInteraction\([\w,]+\))\{.+?\}(logImpression)`, "${1}{return{interactionId:null,pageInstanceId:null};}${2}") - utils.Replace(&input, `(\}logImpression\([\w,]+\))\{.+?\}(logNavigation)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}logNavigation\([\w,]+\))\{.+?\}(getPageInstanceId|getInteractionId)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}handleBackgroundStates\(\))\{.+?\}(startNavigation)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}createLoggingParams\([\w,]+\))\{.+?\}(async pullToLocal)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}initSendingEvents\(\))\{.+?\}(initializeContexts)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}flush\(\))\{.+\}(sendEvents)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}flush\(\w+=!0\))\{.+\}(flushAll)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}send\([\w,:=!\d{}]+\))\{.+?\}(hasContext)`, "${1}{return;}${2}") - utils.Replace(&input, `(\}lastFlush\(\))\{.+?\}(flush\(\))`, "${1}{return;}${2}") + utils.Replace(&input, `sp://logging/v3/\w+`, func(submatches ...string) string { + return "" + }) + utils.Replace(&input, `[^"\/]+\/[^"\/]+\/(public\/)?v3\/events`, func(submatches ...string) string { + return "" + }) + + utils.Replace(&input, `key:"registerEventListeners",value:function\(\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"logInteraction",value:function\([\w,]+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn{interactionId:null,pageInstanceId:null};", submatches[0]) + }) + utils.Replace(&input, `key:"logNonAuthInteraction",value:function\([\w,]+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn{interactionId:null,pageInstanceId:null};", submatches[0]) + }) + utils.Replace(&input, `key:"logImpression",value:function\([\w,]+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"logNonAuthImpression",value:function\([\w,]+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"logNavigation",value:function\([\w,]+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"handleBackgroundStates",value:function\(\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"createLoggingParams",value:function\([\w,]+\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"initSendingEvents",value:function\(\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"flush",value:function\(\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `(\{key:"send",value:function\([\w,]+\))\{[\d\w\s,{}()[\]\.,!\?=>&|;:_""]+?\}(\},\{key:"hasContext")`, func(submatches ...string) string { + return fmt.Sprintf("%s{return;}%s", submatches[1], submatches[2]) + }) + utils.Replace(&input, `key:"lastFlush",value:function\(\)\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `key:"addItemInEventsStorage",value:function\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + + utils.Replace(&input, `registerEventListeners\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `logInteraction\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) + }) + utils.Replace(&input, `logImpression\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `logNavigation\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `handleBackgroundStates\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `initSendingEvents\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `sendEvents\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `storeEvent\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `lastFlush\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) + utils.Replace(&input, `addItemInEventsStorage\([^)]*\)\s*\{`, func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }) return input } func removeRTL(input string) string { - //utils.Replace(&input, `}\[dir=ltr\]\s?`, "} ") - utils.Replace(&input, `html\[dir=ltr\]`, "html") - utils.Replace(&input, `,\s?\[dir=rtl\].+?(\{.+?\})`, "$1") - utils.Replace(&input, `[\w\-\.]+\[dir=rtl\].+?\{.+?\}`, "") - - utils.Replace(&input, `\}\[lang=ar\].+?\{.+?\}`, "}") - utils.Replace(&input, `\}\[dir=rtl\].+?\{.+?\}`, "}") - utils.Replace(&input, `\}html\[dir=rtl\].+?\{.+?\}`, "}") - utils.Replace(&input, `\}html\[lang=ar\].+?\{.+?\}`, "}") - - utils.Replace(&input, `\[lang=ar\].+?\{.+?\}`, "") - utils.Replace(&input, `html\[dir=rtl\].+?\{.+?\}`, "") - utils.Replace(&input, `html\[lang=ar\].+?\{.+?\}`, "") - utils.Replace(&input, `\[dir=rtl\].+?\{.+?\}`, "") + utils.Replace(&input, `}\[dir=ltr\]\s?`, func(submatches ...string) string { + return "} " + }) + utils.Replace(&input, `html\[dir=ltr\]`, func(submatches ...string) string { + return "html" + }) + utils.Replace(&input, `,\s?\[dir=rtl\].+?(\{.+?\})`, func(submatches ...string) string { + return submatches[1] + }) + utils.Replace(&input, `[\w\-\.]+\[dir=rtl\].+?\{.+?\}`, func(submatches ...string) string { + return "" + }) + utils.Replace(&input, `\}\[lang=ar\].+?\{.+?\}`, func(submatches ...string) string { + return "}" + }) + utils.Replace(&input, `\}\[dir=rtl\].+?\{.+?\}`, func(submatches ...string) string { + return "}" + }) + utils.Replace(&input, `\}html\[dir=rtl\].+?\{.+?\}`, func(submatches ...string) string { + return "}" + }) + utils.Replace(&input, `\}html\[lang=ar\].+?\{.+?\}`, func(submatches ...string) string { + return "}" + }) + utils.Replace(&input, `\[lang=ar\].+?\{.+?\}`, func(submatches ...string) string { + return "" + }) + utils.Replace(&input, `html\[dir=rtl\].+?\{.+?\}`, func(submatches ...string) string { + return "" + }) + utils.Replace(&input, `html\[lang=ar\].+?\{.+?\}`, func(submatches ...string) string { + return "" + }) + utils.Replace(&input, `\[dir=rtl\].+?\{.+?\}`, func(submatches ...string) string { + return "" + }) return input } @@ -290,80 +440,93 @@ func exposeAPIs_main(input string) string { utils.Replace( &input, `(?:\w+ |,)([\w$]+)=(\([\w$]+=[\w$]+\.dispatch)`, - `;globalThis.Spicetify.showNotification=(message,isError=false,msTimeout)=>${1}({message,feedbackType:isError?"ERROR":"NOTICE",msTimeout});const ${1}=${2}`) + func(submatches ...string) string { + return fmt.Sprintf(`;globalThis.Spicetify.showNotification=(message,isError=false,msTimeout)=>%s({message,feedbackType:isError?"ERROR":"NOTICE",msTimeout});const %s=%s`, submatches[1], submatches[1], submatches[2]) + }) // Remove list of exclusive shows utils.Replace( &input, `\["spotify:show.+?\]`, - `[]`) + func(submatches ...string) string { + return "[]" + }) // Remove Star Wars easter eggs since it aggressively // listens to keystroke, checking URIs at all time + // TODO: to fix utils.Replace( &input, `\w+\(\)\.createElement\(\w+,\{onChange:this\.handleSaberStateChange\}\),`, - "") + func(submatches ...string) string { + return "" + }) utils.Replace( &input, `"data-testid":`, - `"":`) + func(submatches ...string) string { + return `"":` + }) // Spicetify._platform utils.Replace( &input, `(setTitlebarHeight[\w(){}.,&$!=;"" ]+)(\{version:[\w$]+,)`, - `${1}Spicetify._platform=${2}`) + func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify._platform=%s", submatches[1], submatches[2]) + }) // Redux store utils.Replace( &input, `(,[\w$]+=)(([$\w,.:=;(){}]+\(\{session:[\w$]+,features:[\w$]+,seoExperiment:[\w$]+\}))`, - `${1}Spicetify.Platform.ReduxStore=${2}`) + func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.Platform.ReduxStore=%s", submatches[1], submatches[2]) + }) // React Component: Platform Provider utils.Replace( &input, `(,[$\w]+=)((function\([\w$]{1}\)\{var [\w$]+=[\w$]+\.platform,[\w$]+=[\w$]+\.children,)|(\(\{platform:[\w$]+,children:[\w$]+\}\)=>\{))`, - `${1}Spicetify.ReactComponent.PlatformProvider=${2}`) + func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.ReactComponent.PlatformProvider=%s", submatches[1], submatches[2]) + }) // Prevent breaking popupLyrics utils.Replace( &input, `document.pictureInPictureElement&&\(\w+.current=[!\w]+,document\.exitPictureInPicture\(\)\),\w+\.current=null`, - ``) + func(submatches ...string) string { + return "" + }) // GraphQL definitions utils.Replace( &input, `((?:\w+ ?)?[\w$]+=)(\{kind:"Document",definitions:\[\{(?:\w+:[\w"]+,)+name:\{(?:\w+:[\w"]+,?)+value:("\w+"))`, - `${1}Spicetify.GraphQL.Definitions[${3}]=${2}`) - - // Panel API patch - utils.Replace( - &input, - `(switch\(([\w$])\)\{case [\w$.]+BuddyFeed:return [\w$.]+BuddyFeed;(?:case [\w$.]+:return [\w$.]+;)*)default:`, - `${1}default:return Spicetify.Panel?.hasPanel?.(${2},true)?${2}:0;`) - - // Panel component patch - utils.Replace( - &input, - `case [\w$.]+BuddyFeed:(?:return ?|[\w$]+=)[\w$?]*(?:\([\w$.,]+\)\([\w(){},.:]+)?;(?:break;)?(?:case [\w$.]+:(?:return ?|[\w$]+=)[\w$?]*(?:\([\w$.,]+\)\([\w(){},.:]+)?[\w:]*;(?:break;)?)*default:(?:return ?|[\w$]+=)`, - `${0} Spicetify.Panel?.render()??`) + func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.GraphQL.Definitions[%s]=%s", submatches[1], submatches[3], submatches[2]) + }) utils.Replace( &input, `\b\w\s*\(\)\s*[^;,]*enqueueCustomSnackbar:\s*(\w)\s*[^;]*;`, - `${0}Spicetify.Snackbar.enqueueCustomSnackbar=${1};`) + func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.Snackbar.enqueueCustomSnackbar=%s;", submatches[0], submatches[1]) + }) utils.Replace( &input, `\(\({[^}]*,\s*imageSrc`, - `Spicetify.Snackbar.enqueueImageSnackbar=${0}`) + func(submatches ...string) string { + return fmt.Sprintf("Spicetify.Snackbar.enqueueImageSnackbar=%s", submatches[0]) + }) // Menu hook - utils.Replace(&input, `("Menu".+?children:)([\w$][\w$\d]*)`, `${1}[Spicetify.ContextMenuV2.renderItems(),${2}].flat()`) + utils.Replace(&input, `("Menu".+?children:)([\w$][\w$\d]*)`, func(submatches ...string) string { + return fmt.Sprintf("%s[Spicetify.ContextMenuV2.renderItems(),%s].flat()", submatches[1], submatches[2]) + }) croppedInput := utils.FindFirstMatch(input, `"context-menu".*value:"contextmenu"`)[0] @@ -384,7 +547,9 @@ func exposeAPIs_main(input string) string { target = utils.FindFirstMatch(croppedInput, `triggerRef:([\w_$]+)`)[1] } - utils.Replace(&input, `\(0,([\w_$]+)\.jsx\)\([\w_$]+\.[\w_$]+,\{value:"contextmenu"[^\}]+\}\)\}\)`, `(0,${1}.jsx)((Spicetify.ContextMenuV2._context||(Spicetify.ContextMenuV2._context=`+react+`.createContext(null))).Provider,{value:{props:`+menu+`?.props,trigger:`+trigger+`,target:`+target+`},children:${0}})`) + utils.Replace(&input, `\(0,([\w_$]+)\.jsx\)\([\w_$]+\.[\w_$]+,\{value:"contextmenu"[^\}]+\}\)\}\)`, func(submatches ...string) string { + return fmt.Sprintf("(0,%s.jsx)((Spicetify.ContextMenuV2._context||(Spicetify.ContextMenuV2._context=%s.createContext(null))).Provider,{value:{props:%s?.props,trigger:%s,target:%s},children:%s})", submatches[1], react, menu, trigger, target, submatches[0]) + }) return input } @@ -394,7 +559,9 @@ func exposeAPIs_vendor(input string) string { utils.Replace( &input, `,(\w+)\.prototype\.toAppType`, - `,(globalThis.Spicetify.URI=${1})${0}`) + func(submatches ...string) string { + return fmt.Sprintf(`,(globalThis.Spicetify.URI=%s)%s`, submatches[1], submatches[0]) + }) // URI after 1.2.4 if !strings.Contains(input, "Spicetify.URI") { @@ -409,13 +576,13 @@ func exposeAPIs_vendor(input string) string { if URIObj[1] == "" { URIObj[1] = URIObj[2] // Class is a self-invoking function - URI = URI + "()" + URI = fmt.Sprintf("%s()", URI) } input = strings.Replace( input, URI, - URI+";Spicetify.URI="+URIObj[1]+";", + fmt.Sprintf("%s;Spicetify.URI=%s;", URI, URIObj[1]), 1) } } @@ -423,61 +590,52 @@ func exposeAPIs_vendor(input string) string { utils.ReplaceOnce( &input, `\(function\(\w+\)\{return \w+\.variant\?function\(\w+\)\{`, - `Spicetify._fontStyle=${0}`) + func(submatches ...string) string { + return fmt.Sprintf("Spicetify._fontStyle=%s", submatches[0]) + }) utils.ReplaceOnce( &input, `=(?:\(\w\)=>|function\(\w\)\{)\w+ ?\w=\w\.iconSize`, - `=Spicetify.ReactComponent.IconComponent${0}`) + func(submatches ...string) string { + return fmt.Sprintf("=Spicetify.ReactComponent.IconComponent%s", submatches[0]) + }) // Mapping styled-components classes utils.Replace( &input, `(\w+ [\w$_]+)=[\w$_]+\([\w$_]+>>>0\)`, - `${1}=Spicetify._getStyledClassName(arguments,this)`) + func(submatches ...string) string { + return fmt.Sprintf("%s=Spicetify._getStyledClassName(arguments,this)", submatches[1]) + }) // Tippy utils.Replace( &input, - `([\w$_]+)\.setDefaultProps=`, - `Spicetify.Tippy=${1};${0}`) + `([\w\$_]+)\.setDefaultProps=`, + func(submatches ...string) string { + return fmt.Sprintf("Spicetify.Tippy=%s;%s", submatches[1], submatches[0]) + }) // Flipper components utils.Replace( &input, `([\w$]+)=((?:function|\()([\w$.,{}()= ]+(?:springConfig|overshootClamping)){2})`, - `${1}=Spicetify.ReactFlipToolkit.spring=${2}`) + func(submatches ...string) string { + return fmt.Sprintf("%s=Spicetify.ReactFlipToolkit.spring=%s", submatches[1], submatches[2]) + }) // Snackbar https://github.com/iamhosseindhv/notistack utils.Replace( &input, `\w+\s*=\s*\w\.call\(this,[^)]+\)\s*\|\|\s*this\)\.enqueueSnackbar`, - `Spicetify.Snackbar=${0}`) + func(submatches ...string) string { + return fmt.Sprintf("Spicetify.Snackbar=%s", submatches[0]) + }) return input } -// Disable WebUI by redirect to zlink app -// so when Spotify forces user to use xpui, it loads zlink instead. -func fakeZLink(dest string) { - os.MkdirAll(dest, 0700) - entryFile := filepath.Join(dest, "index.html") - html := ` - -` - manifestFile := filepath.Join(dest, "manifest.json") - manifest := ` -{ - "BundleIdentifier": "zlink", - "BundleType": "Application" -} -` - os.WriteFile(entryFile, []byte(html), 0700) - os.WriteFile(manifestFile, []byte(manifest), 0700) -} - type githubRelease = utils.GithubRelease func splitVersion(version string) ([3]int, error) { diff --git a/src/utils/utils.go b/src/utils/utils.go index 17b3779b3f..2298b7385b 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -164,18 +164,25 @@ func CopyFile(srcPath, dest string) error { // Replace uses Regexp to find any matched from `input` with `regexpTerm` // and replaces them with `replaceTerm` then returns new string. -func Replace(input *string, regexpTerm string, replaceTerm string) { - re := regexp.MustCompile(regexpTerm) - *input = re.ReplaceAllString(*input, replaceTerm) +func Replace(str *string, pattern string, repl func(submatches ...string) string) { + re := regexp.MustCompile(pattern) + *str = re.ReplaceAllStringFunc(*str, func(match string) string { + submatches := re.FindStringSubmatch(match) + return repl(submatches...) + }) } -func ReplaceOnce(input *string, regexpTerm string, replaceTerm string) { - re := regexp.MustCompile(regexpTerm) - matches := re.FindAllString(*input, -1) - if len(matches) > 0 { - toReplace := re.ReplaceAllString(matches[0], replaceTerm) - *input = strings.Replace(*input, matches[0], toReplace, 1) - } +func ReplaceOnce(str *string, pattern string, repl func(submatches ...string) string) { + re := regexp.MustCompile(pattern) + firstMatch := true + *str = re.ReplaceAllStringFunc(*str, func(match string) string { + if firstMatch { + firstMatch = false + submatches := re.FindStringSubmatch(match) + return repl(submatches...) + } + return match + }) } func FindMatch(input string, regexpTerm string) [][]string {