Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(grid): sorting options #545

Merged
merged 17 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
theRealPadster marked this conversation as resolved.
Show resolved Hide resolved
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);
theRealPadster marked this conversation as resolved.
Show resolved Hide resolved
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;
};