Skip to content

Commit

Permalink
feat(grid): sorting options (#545)
Browse files Browse the repository at this point in the history
* Begin implementing sortbox

* Finish sorting options for extensions

* Add support for themes

* Update src/components/Grid.tsx

Co-authored-by: Isaac <[email protected]>

* 🚧 Add SortMode type, make `sortCardItems` generic, add back in `this.requestQueue.length...` bit

* 🔥 Remove empty file

* Fix grid header not being sticky

* ♻️ Remove "spicetify marketplace" text in grid header, add "sort by" label, and move sort box to left

* Change order of sort by dropdown options

* Add jsdocs for "hideInstalled" param

* ✨ Add sort functionality to apps tab

* ✨ Add sort support for Installed tab

* ✨ Add sort functionality to Snippets tab

* Add note about hiding sort options for snippets

* ✨ 🌐 Add i18n support for sort dropdown

---------

Co-authored-by: Isaac <[email protected]>
  • Loading branch information
CharlieS1103 and theRealPadster authored Oct 11, 2023
1 parent b97c715 commit 7201e23
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 37 deletions.
1 change: 1 addition & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class App extends React.Component<{
schemes,
activeScheme,
},
sort: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.sort, "stars"),
};

if (!this.CONFIG.activeTab || !this.CONFIG.tabs.filter(tab => tab.name === this.CONFIG.activeTab).length) {
Expand Down
97 changes: 69 additions & 28 deletions src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { Option } from "react-dropdown";
const Spicetify = window.Spicetify;

import { CardItem, CardType, Config, SchemeIni, Snippet, TabItemConfig } from "../types/marketplace-types";
import { getLocalStorageDataFromKey, generateSchemesOptions, injectColourScheme } from "../logic/Utils";
import { getLocalStorageDataFromKey,
generateSchemesOptions,
injectColourScheme,
generateSortOptions,
sortCardItems,
} from "../logic/Utils";
import { LOCALSTORAGE_KEYS, ITEMS_PER_REQUEST, MARKETPLACE_VERSION, LATEST_RELEASE } from "../constants";
import { openModal } from "../logic/LaunchModals";
import {
Expand Down Expand Up @@ -52,7 +57,7 @@ class Grid extends React.Component<

// Fetches the sorting options, fetched from SortBox.js
this.sortConfig = {
by: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.sortBy, "top"),
by: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.sort, "top"),
};

this.state = {
Expand Down Expand Up @@ -132,7 +137,7 @@ class Grid extends React.Component<
updateSort(sortByValue) {
if (sortByValue) {
this.sortConfig.by = sortByValue;
localStorage.setItem(LOCALSTORAGE_KEYS.sortBy, sortByValue);
localStorage.setItem(LOCALSTORAGE_KEYS.sort, sortByValue);
}

// this.requestPage = null;
Expand Down Expand Up @@ -187,8 +192,9 @@ class Grid extends React.Component<
switch (activeTab) {
case "Extensions": {
const pageOfRepos = await getTaggedRepos("spicetify-extensions", this.requestPage, this.BLACKLIST);
const extensions: CardItem[] = [];
for (const repo of pageOfRepos.items) {
const extensions = await fetchExtensionManifest(
const repoExtensions = await fetchExtensionManifest(
repo.contents_url,
repo.default_branch,
repo.stargazers_count,
Expand All @@ -201,14 +207,18 @@ class Grid extends React.Component<
return -1;
}

if (extensions && extensions.length) {
// console.log(`${repo.name} has ${extensions.length} extensions:`, extensions);
extensions.forEach((extension) => {
Object.assign(extension, { lastUpdated: repo.pushed_at });
this.appendCard(extension, "extension", activeTab);
});
if (repoExtensions && repoExtensions.length) {
extensions.push(...repoExtensions.map((extension) => ({
...extension, lastUpdated: repo.pushed_at,
})));
}
}

sortCardItems(extensions, localStorage.getItem("marketplace:sort") || "stars");

for (const extension of extensions) {
this.appendCard(extension, "extension", activeTab);
}
this.setState({ cards: this.cardList });

// First result is null or -1 so it coerces to 1
Expand All @@ -231,17 +241,24 @@ class Grid extends React.Component<

for (const type in installedStuff) {
if (installedStuff[type].length) {
const installedOfType: CardItem[] = [];
installedStuff[type].forEach(async (itemKey) => {
// TODO: err handling
const extension = getLocalStorageDataFromKey(itemKey);
const installedItem = getLocalStorageDataFromKey(itemKey);
// I believe this stops the requests when switching tabs?
if (this.requestQueue.length > 1 && queue !== this.requestQueue[0]) {
// Stop this queue from continuing to fetch and append to cards list
return -1;
}

this.appendCard(extension, type as CardType, activeTab);
installedOfType.push(installedItem);
});

sortCardItems(installedOfType, localStorage.getItem("marketplace:sort") || "stars");

for (const item of installedOfType) {
this.appendCard(item, type as CardType, activeTab);
}
}
}
this.setState({ cards: this.cardList });
Expand All @@ -251,28 +268,37 @@ class Grid extends React.Component<
// installed extension do them all in one go, since it's local
} case "Themes": {
const pageOfRepos = await getTaggedRepos("spicetify-themes", this.requestPage, this.BLACKLIST);
const themes: CardItem[] = [];
for (const repo of pageOfRepos.items) {

const themes = await fetchThemeManifest(
const repoThemes = await fetchThemeManifest(
repo.contents_url,
repo.default_branch,
repo.stargazers_count,
);

// I believe this stops the requests when switching tabs?
if (this.requestQueue.length > 1 && queue !== this.requestQueue[0]) {
// Stop this queue from continuing to fetch and append to cards list
return -1;
}

if (themes && themes.length) {
themes.forEach((theme) => {
Object.assign(theme, { lastUpdated: repo.pushed_at });
this.appendCard(theme, "theme", activeTab);
});
if (repoThemes && repoThemes.length) {
themes.push(...repoThemes.map(
(theme) => ({
...theme,
lastUpdated: repo.pushed_at,
}),
));
}
}
this.setState({ cards: this.cardList });

sortCardItems(themes, localStorage.getItem("marketplace:sort") || "stars");

for (const theme of themes) {
this.appendCard(theme, "theme", activeTab);
}

// First request is null, so coerces to 1
const currentPage = this.requestPage > -1 && this.requestPage ? this.requestPage : 1;
// -1 because the page number is 1-indexed
Expand All @@ -283,11 +309,13 @@ class Grid extends React.Component<
if (remainingResults > 0) return currentPage + 1;
else console.debug("No more theme results");
break;
} case "Apps": {
}
case "Apps": {
const pageOfRepos = await getTaggedRepos("spicetify-apps", this.requestPage, this.BLACKLIST);
for (const repo of pageOfRepos.items) {
const apps: CardItem[] = [];

const apps = await fetchAppManifest(
for (const repo of pageOfRepos.items) {
const repoApps = await fetchAppManifest(
repo.contents_url,
repo.default_branch,
repo.stargazers_count,
Expand All @@ -298,15 +326,21 @@ class Grid extends React.Component<
return -1;
}

if (apps && apps.length) {
apps.forEach((app) => {
Object.assign(app, { lastUpdated: repo.pushed_at });
this.appendCard(app, "app", activeTab);
});
if (repoApps && repoApps.length) {
apps.push(...repoApps.map((app) => ({
...app,
lastUpdated: repo.pushed_at,
})));
}
}
this.setState({ cards: this.cardList });

sortCardItems(apps, localStorage.getItem("marketplace:sort") || "stars");

for (const app of apps) {
this.appendCard(app, "app", activeTab);
}

// First request is null, so coerces to 1
const currentPage = this.requestPage > -1 && this.requestPage ? this.requestPage : 1;
// -1 because the page number is 1-indexed
Expand All @@ -324,7 +358,9 @@ class Grid extends React.Component<
// Stop this queue from continuing to fetch and append to cards list
return -1;
}

if (snippets && snippets.length) {
sortCardItems(snippets, localStorage.getItem("marketplace:sort") || "stars");
snippets.forEach((snippet) => this.appendCard(snippet, "snippet", activeTab));
this.setState({ cards: this.cardList });
}
Expand Down Expand Up @@ -501,7 +537,6 @@ class Grid extends React.Component<
<section className="contentSpacing">
<div className="marketplace-header">
<div className="marketplace-header__left">
<h1>{this.props.title}</h1>
{this.state.newUpdate
? <button type="button" title={t("grid.newUpdate")} className="marketplace-header-icon-button" id="marketplace-update"
onClick={() => window.location.href = "https://github.com/spicetify/spicetify-marketplace/releases/latest"}
Expand All @@ -510,6 +545,12 @@ class Grid extends React.Component<
&nbsp;{this.state.version}
</button>
: null}
{/* Generate a new box for sorting options */}
<h2 className="marketplace-header__label">{t("grid.sort.label")}</h2>
<SortBox
onChange={(value) => this.updateSort(value)}
sortBoxOptions={generateSortOptions(t)}
sortBySelectedFn={(a) => a.key === this.CONFIG.sort} />
</div>
<div className="marketplace-header__right">
{/* Show theme developer tools button if themeDevTools is enabled */}
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const LOCALSTORAGE_KEYS = {
installedThemes: "marketplace:installed-themes",
activeTab: "marketplace:active-tab",
tabs: "marketplace:tabs",
sortBy: "marketplace:sort-by",
sort: "marketplace:sort",
// Theme installed store the localsorage key of the theme (e.g. marketplace:installed:NYRI4/Comfy-spicetify/user.css)
themeInstalled: "marketplace:theme-installed",
albumArtBasedColor: "marketplace:albumArtBasedColors",
Expand Down
2 changes: 2 additions & 0 deletions src/logic/FetchRemotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async function getRepoManifest(user: string, repo: string, branch: string) {
* @param contents_url The repo's GitHub API contents_url (e.g. "https://api.github.com/repos/theRealPadster/spicetify-hide-podcasts/contents/{+path}")
* @param branch The repo's default branch (e.g. main or master)
* @param stars The number of stars the repo has
* @param hideInstalled Whether to hide installed items or not (defaults to `false`)
* @returns Extension info for card (or null)
*/
export async function fetchExtensionManifest(contents_url: string, branch: string, stars: number, hideInstalled = false) {
Expand Down Expand Up @@ -317,3 +318,4 @@ export const fetchCssSnippets = async () => {
}, []);
return snippets;
};

65 changes: 65 additions & 0 deletions src/logic/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,26 @@ export const generateSchemesOptions = (schemes: SchemeIni) => {
));
};

/**
* Generate a list of options for the sort dropdown
* @param t The string translation function
* @returns The sort options for the sort dropdown
*/
export const generateSortOptions = (t: (key: string) => string) => {
// TODO: It would be great if I could disable the options that don't apply for snippets
// But it looks like that's not supported by the library
// https://github.com/fraserxu/react-dropdown/pull/176
// TODO: I could also just remove the options for snippets,
// but then the sort resets when you switch tabs and it's disruptive

return [
{ key: "stars", value: t("grid.sort.stars") },
{ key: "newest", value: t("grid.sort.newest") },
{ key: "oldest", value: t("grid.sort.oldest") },
{ key: "a-z", value: t("grid.sort.aToZ") },
{ key: "z-a", value: t("grid.sort.zToA") },
];
};
/**
* Reset Marketplace localStorage keys
* @param categories The categories to reset. If none provided, reset everything.
Expand Down Expand Up @@ -582,6 +602,51 @@ export const addExtensionToSpicetifyConfig = (main?: string) => {
}
};

/**
* Compare two card items/snippets by name.
* This will use `title` for snippets and `manifest.name` for everything else.
*/
const compareNames = (a: CardItem | Snippet, b: CardItem | Snippet) => {
// Snippets have a title, but no manifest
const aName = a.title || a?.manifest?.name || "";
const bName = b.title || b?.manifest?.name || "";
return aName.localeCompare(bName);
};

/**
* Compare two card items/snippets by lastUpdated.
* This is skipped for snippets, since they don't have a lastUpdated property.
*/
const compareUpdated = (a: CardItem | Snippet, b: CardItem | Snippet) => {
// Abort compare if items are missing lastUpdated
if (a.lastUpdated === undefined || b.lastUpdated === undefined) return 0;

const aDate = new Date(a.lastUpdated);
const bDate = new Date(b.lastUpdated);
return bDate.getTime() - aDate.getTime();
};

export const sortCardItems = (cardItems: CardItem[] | Snippet[], sortMode: string) => {
switch (sortMode) {
case "a-z":
cardItems.sort((a, b) => compareNames(a, b));
break;
case "z-a":
cardItems.sort((a, b) => compareNames(b, a));
break;
case "newest":
cardItems.sort((a, b) => compareUpdated(a, b));
break;
case "oldest":
cardItems.sort((a, b) => compareUpdated(b, a));
break;
case "stars":
default:
cardItems.sort((a, b) => b.stars - a.stars);
break;
}
};

// Make a ping to the jsdelivr CDN to check if the user has an internet connection
export async function getAvailableTLD() {
const tlds = ["net", "xyz"];
Expand Down
10 changes: 9 additions & 1 deletion src/resources/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,15 @@
"lastUpdated": "Last updated {{val, datetime}}",
"externalJS": "external JS",
"dark": "dark",
"light": "light"
"light": "light",
"sort": {
"label": "Sort by:",
"stars": "Stars",
"newest": "Newest",
"oldest": "Oldest",
"aToZ": "A-Z",
"zToA": "Z-A"
}
},
"readmePage": {
"title": "$t(grid.spicetifyMarketplace) - Readme",
Expand Down
12 changes: 6 additions & 6 deletions src/styles/components/_grid.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
@use "../constants.scss";

// Compatibility with new Spotify layout
.Root__fixed-top-bar ~ .Root__main-view .marketplace-header {
padding-top: 64px;
}

.marketplace-header {
-webkit-box-pack: justify;
-webkit-box-align: center;
Expand All @@ -18,7 +13,7 @@
// To position the settings button + colour schemes
position: sticky;
flex-direction: row-reverse;
// top: 80px;
top: 80px;
z-index: 1;
}

Expand All @@ -35,6 +30,11 @@
left: 0;
}

.marketplace-header__label {
display: inline-flex;
align-self: center;
}

.marketplace-grid {
--minimumColumnWidth: 180px;
--column-width: minmax(var(--minimumColumnWidth), 1fr);
Expand Down
7 changes: 6 additions & 1 deletion src/types/marketplace-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export type CardItem = {
stars: number;
tags: string[];
lastUpdated: string;

name: string;
lastUpdated: string;
stargazers_count: number;
// For themes only
cssURL?: string;
schemesURL?: string;
Expand Down Expand Up @@ -151,6 +153,8 @@ export type SchemeIni = {
[key: string]: ColourScheme;
};

export type SortMode = "a-z" | "z-a" | "newest" | "oldest" | "stars";

export type Config = {
// Fetch the settings and set defaults. Used in Settings.js
visual: VisualConfig,
Expand All @@ -161,4 +165,5 @@ export type Config = {
schemes?: SchemeIni;
activeScheme?: string | null;
},
sort: SortMode;
};

0 comments on commit 7201e23

Please sign in to comment.