Skip to content

Commit

Permalink
Added shared libs
Browse files Browse the repository at this point in the history
  • Loading branch information
Bergbok committed Mar 6, 2024
1 parent e9c5ae0 commit 996d1c0
Show file tree
Hide file tree
Showing 37 changed files with 2,430 additions and 497 deletions.
159 changes: 159 additions & 0 deletions libs/shared/src/components/nav-bar-link.component.tsx
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>
</>
);
}
}
26 changes: 26 additions & 0 deletions libs/shared/src/cosmos/models/query-parameters.ts
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;
};
42 changes: 42 additions & 0 deletions libs/shared/src/cosmos/utils/build-url.ts
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;
}
15 changes: 15 additions & 0 deletions libs/shared/src/debug/list-icons.ts
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();
}
79 changes: 79 additions & 0 deletions libs/shared/src/debug/register-proxy.ts
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);
}
Loading

0 comments on commit 996d1c0

Please sign in to comment.