-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
37 changed files
with
2,430 additions
and
497 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { getPlatformApiOrThrow } from '@shared/utils/spicetify-utils'; | ||
import type { History, HistoryEntry } from '../platform/history'; | ||
import React, { useEffect, useState } from 'react'; | ||
import type { LocalStorageAPI } from '@shared/platform/local-storage'; | ||
|
||
export type NavBarLinkProps = { | ||
icon: JSX.Element; | ||
activeIcon: JSX.Element; | ||
href: string; | ||
label: string; | ||
}; | ||
|
||
export function NavBarLink(props: Readonly<NavBarLinkProps>): JSX.Element { | ||
const history = getPlatformApiOrThrow<History>('History'); | ||
const initialActive = history.location.pathname === props.href; | ||
const sidebar = document.querySelector<HTMLDivElement>('.Root__nav-bar'); | ||
|
||
if (sidebar == null) { | ||
throw new Error('Could not find sidebar'); | ||
} | ||
|
||
const [active, setActive] = useState(initialActive); | ||
const [isLibX, setIsLibX] = useState(isLibraryXEnabled(sidebar)); | ||
const [isCollapsed, setIsCollapsed] = useState(isSideBarCollapsed()); | ||
|
||
useEffect(() => { | ||
function handleHistoryChange(e: HistoryEntry): void { | ||
setActive(e.pathname === props.href); | ||
} | ||
|
||
const unsubscribe = history.listen(handleHistoryChange); | ||
return unsubscribe; | ||
}, []); | ||
|
||
useEffect(() => { | ||
// From https://github.dev/spicetify/spicetify-cli/blob/master/jsHelper/sidebarConfig.js | ||
// Check if library X has been enabled / disabled in experimental settings | ||
const observer = new MutationObserver((mutations) => { | ||
for (const mutation of mutations) { | ||
if (mutation.attributeName === 'class') { | ||
if (isLibraryXEnabled(mutation.target as HTMLElement)) { | ||
setIsLibX(true); | ||
} else { | ||
setIsLibX(false); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
observer.observe(sidebar, { | ||
childList: true, | ||
attributes: true, | ||
attributeFilter: ['class'], | ||
}); | ||
|
||
return () => { | ||
observer.disconnect(); | ||
}; | ||
}, []); | ||
|
||
useEffect(() => { | ||
// Observe sidebar width changes | ||
const observer = new ResizeObserver(() => { | ||
setIsCollapsed(isSideBarCollapsed()); | ||
}); | ||
|
||
observer.observe(sidebar); | ||
|
||
return () => { | ||
observer.disconnect(); | ||
}; | ||
}, []); | ||
|
||
function navigate(): void { | ||
history.push(props.href); | ||
} | ||
|
||
if (sidebar == null) { | ||
return <></>; | ||
} | ||
|
||
function isSideBarCollapsed(): boolean { | ||
return ( | ||
getPlatformApiOrThrow<LocalStorageAPI>('LocalStorageAPI').getItem( | ||
'ylx-sidebar-state', | ||
) === 1 | ||
); | ||
} | ||
|
||
function isLibraryXEnabled(sidebar: HTMLElement): boolean { | ||
return ( | ||
sidebar.classList.contains('hasYLXSidebar') || | ||
!!sidebar.querySelector('.main-yourLibraryX-entryPoints') | ||
); | ||
} | ||
|
||
if (isLibX) { | ||
const link = ( | ||
<a | ||
draggable="false" | ||
href="#" | ||
aria-label={props.label} | ||
className={`link-subtle main-yourLibraryX-navLink ${ | ||
active ? 'main-yourLibraryX-navLinkActive active' : '' | ||
}`} | ||
onClick={navigate} | ||
> | ||
{props.icon} | ||
{props.activeIcon} | ||
{!isCollapsed && ( | ||
<span className="TypeElement-balladBold-type"> | ||
{props.label} | ||
</span> | ||
)} | ||
</a> | ||
); | ||
|
||
return ( | ||
<li | ||
className="main-yourLibraryX-navItem InvalidDropTarget" | ||
data-id={props.href} | ||
> | ||
{isCollapsed ? ( | ||
<Spicetify.ReactComponent.TooltipWrapper | ||
label={props.label} | ||
showDelay={100} | ||
placement="right" | ||
> | ||
{link} | ||
</Spicetify.ReactComponent.TooltipWrapper> | ||
) : ( | ||
link | ||
)} | ||
</li> | ||
); | ||
} else { | ||
return ( | ||
<> | ||
<li className="main-navBar-navBarItem" data-id={props.href}> | ||
<a | ||
draggable="false" | ||
className={`link-subtle main-navBar-navBarLink ${ | ||
active ? 'main-navBar-navBarLinkActive active' : '' | ||
}`} | ||
onClick={navigate} | ||
> | ||
<div className="icon collection-icon">{props.icon}</div> | ||
<div className="icon collection-active-icon"> | ||
{props.activeIcon} | ||
</div> | ||
<span className="ellipsis-one-line main-type-mestoBold"> | ||
{props.label} | ||
</span> | ||
</a> | ||
</li> | ||
</> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export type FilterParameter<T> = { | ||
property: keyof T; | ||
value: any; | ||
operator: | ||
| 'eq' | ||
| 'ne' | ||
| 'lt' | ||
| 'le' | ||
| 'gt' | ||
| 'ge' | ||
| 'contains' | ||
| 'startsWith' | ||
| 'bitMask'; | ||
}; | ||
|
||
export type SortParameter<T> = { | ||
property: keyof T; | ||
desc?: true; | ||
}; | ||
|
||
export type QueryParameter<T> = { | ||
filters?: FilterParameter<T>[]; | ||
sort?: SortParameter<T>[]; | ||
start?: number; | ||
length?: number; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import type { QueryParameter } from '../models/query-parameters'; | ||
|
||
export function buildQueryString<T>(parameters: QueryParameter<T>): string { | ||
const parts: string[] = []; | ||
|
||
if (parameters.sort !== undefined && parameters.sort.length > 0) { | ||
const options = parameters.sort | ||
.map((o) => | ||
o.desc === true ? `${String(o.property)} DESC` : o.property, | ||
) | ||
.join(','); | ||
|
||
parts.push(`sort=${encodeURIComponent(options)}`); | ||
} | ||
|
||
if (parameters.filters !== undefined && parameters.filters.length > 0) { | ||
const options = parameters.filters | ||
.map((f) => `${String(f.property)} ${f.operator} ${f.value}`) | ||
.join(','); | ||
|
||
parts.push(`filter=${encodeURIComponent(options)}`); | ||
} | ||
|
||
if (parameters.start !== undefined) { | ||
parts.push(`start=${parameters.start}`); | ||
} | ||
|
||
if (parameters.length !== undefined) { | ||
parts.push(`length=${parameters.length}`); | ||
} | ||
|
||
return parts.join('&'); | ||
} | ||
|
||
export function buildUrl<T>( | ||
url: string, | ||
parameters?: QueryParameter<T>, | ||
): string { | ||
const queryString = | ||
parameters !== undefined ? '?' + buildQueryString(parameters) : ''; | ||
return url + queryString; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* List all the existing icons in the context menu. | ||
*/ | ||
export function listIcons(): void { | ||
const items = Object.entries(Spicetify.SVGIcons).map(([iconName, icon]) => { | ||
return new Spicetify.ContextMenu.Item( | ||
iconName, | ||
() => {}, | ||
() => true, | ||
`<svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor">${icon}</svg>` as any, | ||
); | ||
}); | ||
|
||
new Spicetify.ContextMenu.SubMenu('Icons', items, () => true).register(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
function createProxyHandler<T extends object>( | ||
objectName: string, | ||
): ProxyHandler<T> { | ||
const handler = { | ||
get(target: T, property: string, receiver: any) { | ||
const targetValue = Reflect.get(target, property, receiver); | ||
|
||
if (typeof targetValue === 'function') { | ||
return function (...args: unknown[]) { | ||
const result = targetValue.apply(this, args); | ||
console.log( | ||
`[${objectName}] - CALL`, | ||
property, | ||
args, | ||
`-->`, | ||
result, | ||
); | ||
return result; | ||
}; | ||
} else { | ||
console.log( | ||
`[${objectName}] - GET`, | ||
property, | ||
'-->', | ||
targetValue, | ||
); | ||
return targetValue; | ||
} | ||
}, | ||
}; | ||
|
||
return handler; | ||
} | ||
|
||
/** | ||
* Register a proxy around an instance of an object. | ||
* @param object The object to spy on. | ||
* @param objectName A name to be printed to the console. | ||
*/ | ||
export function registerProxy<T>(object: T, objectName: string): void { | ||
const prototype = Object.create(Object.getPrototypeOf(object)); | ||
Object.setPrototypeOf( | ||
object, | ||
new Proxy(prototype, createProxyHandler(objectName)), | ||
); | ||
|
||
console.log(`Registered proxy for ${objectName}.`); | ||
} | ||
|
||
/** | ||
* Wrap all APIs exposed by Spicetify.Platform with a proxy. | ||
*/ | ||
export function registerPlatformProxies(): void { | ||
for (const [name, api] of Object.entries(Spicetify.Platform)) { | ||
registerProxy(api, name); | ||
} | ||
} | ||
|
||
export function registerServicesProxies(): void { | ||
const servicesMap = new Map<string, any>(); | ||
|
||
for (const [platformName, platformApi] of Object.entries( | ||
Spicetify.Platform, | ||
)) { | ||
for (const [name, service] of Object.entries(platformApi as any).filter( | ||
([n, s]) => n.startsWith('_'), | ||
)) { | ||
const fullName = `${platformName}.${name}`; | ||
if (!servicesMap.has(name)) { | ||
servicesMap.set(name, service); | ||
try { | ||
registerProxy(service, fullName); | ||
} catch {} | ||
} | ||
} | ||
} | ||
|
||
console.log(servicesMap); | ||
} |
Oops, something went wrong.