[feat-216] support for sorting playlists

This commit is contained in:
silentrald
2024-10-07 22:51:12 +08:00
parent 61b728ed55
commit b4874eced4
13 changed files with 312 additions and 40 deletions
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "Hochladedatum"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Lade Mods...",
"no-internet": "Kein Internet",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "Date Uploaded"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Loading mods...",
"no-internet": "No internet",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "Fecha de subida"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Cargando mods...",
"no-internet": "Sin internet",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "Date de téléchargement"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Chargement des mods...",
"no-internet": "Pas d'accès Internet",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "アップロード日"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Modを読み込み中...",
"no-internet": "インターネット接続がありません!",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "Дата загрузки"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Загрузка модов...",
"no-internet": "Нет интернета",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "上傳日期"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Mod 載入中...",
"no-internet": "無網路",
+10
View File
@@ -115,6 +115,16 @@
"date-uploaded": "上传日期"
}
},
"playlists": {
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"min-notes-per-second": "Min Notes per Second",
"max-notes-per-second": "Max Notes per Second"
}
},
"mods": {
"loading-mods": "Mod 加载中...",
"no-internet": "无网络",
@@ -20,13 +20,14 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
import { LocalPlaylistFilter, LocalPlaylistFilterPanel, LocalPlaylistSort } from "./playlists/local-playlist-filter-panel.component";
import { noop } from "shared/helpers/function.helpers";
import { Dropzone } from "../shared/dropzone.component";
import { logRenderError } from "renderer";
import { BsmSelect, BsmSelectOption } from "../shared/bsm-select.component";
import { MapsSorterService } from "renderer/services/maps/sorter.service";
import { BsmButton } from "../shared/bsm-button.component";
import { PlaylistsSorterService } from "renderer/services/playlists/sorter.service";
type Props = {
readonly version?: BSVersion;
@@ -42,11 +43,15 @@ export const InstalledMapsContext = createContext<{
export function MapsPlaylistsPanel({ version, isActive }: Props) {
const MAP_TAB = 0;
const PLAYLIST_TAB = 1;
const mapsManager = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
const mapsSorter = useService(MapsSorterService);
const playlistsManager = useService(PlaylistsManagerService);
const playlistsDownloader = useService(PlaylistDownloaderService);
const playlistsSorter = useService(PlaylistsSorterService);
const t = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
@@ -73,6 +78,18 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
ascending: true,
});
const [playlistFilter, setPlaylistFilter] = useState<LocalPlaylistFilter>({});
const [playlistSort, setPlaylistSort] = useState<LocalPlaylistSort>({
compare: playlistsSorter.getDefaultComparator(),
ascending: true,
});
const [selectedSort, setSelectedSort] = useState<string>("name");
const [sortOptions, setSortOptions] = useState<BsmSelectOption<string>[]>(
() => mapsSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.maps.sort.${key}`,
value: key,
}))
);
const [search, setSearch] = useState("");
const mapsLinkedState = useObservable(() => {
@@ -108,6 +125,39 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return mapsManager.unlinkVersion(version);
};
const handleTabChange = (index: number) => {
setTabIndex(index);
switch (index) {
case MAP_TAB:
setSortOptions(() => mapsSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.maps.sort.${key}`,
value: key,
})));
setSelectedSort(mapsSorter.getDefaultComparatorKey());
setMapSort({
compare: mapsSorter.getDefaultComparator(),
ascending: playlistSort.ascending,
});
break;
case PLAYLIST_TAB:
setSortOptions(() => playlistsSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.playlists.sort.${key}`,
value: key,
})));
setSelectedSort(playlistsSorter.getDefaultComparatorKey());
setPlaylistSort({
compare: playlistsSorter.getDefaultComparator(),
ascending: mapSort.ascending,
});
break;
default:
break;
}
};
const handlePlaylistLinkClick = () => {
if(playlistLinkedState === FolderLinkState.Pending || playlistLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
@@ -143,22 +193,50 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
];
})();
const sortOptions: BsmSelectOption<string>[] = useConstant(() => {
return mapsSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.maps.sort.${key}`,
value: key,
}));
});
const handleSortChange = (key: string) => {
setMapSort({
compare: mapsSorter.getComparator(key),
ascending: mapSort.ascending,
});
switch (tabIndex) {
case MAP_TAB:
setMapSort({
compare: mapsSorter.getComparator(key),
ascending: mapSort.ascending,
});
break;
case PLAYLIST_TAB:
setPlaylistSort({
compare: playlistsSorter.getComparator(key),
ascending: playlistSort.ascending,
});
break;
default:
break;
}
}
const handleAscendingClick = () => {
switch (tabIndex) {
case MAP_TAB:
setMapSort({
compare: mapSort.compare,
ascending: !mapSort.ascending,
});
break;
case PLAYLIST_TAB:
setPlaylistSort({
compare: playlistSort.compare,
ascending: !playlistSort.ascending,
});
break;
default:
break;
}
}
const dropDownItems = ((): DropDownItem[] => {
if (tabIndex === 0) {
if (tabIndex === MAP_TAB) {
return [
{ icon: "export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.() },
{ icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.() },
@@ -195,7 +273,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<input
type="text"
className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2"
placeholder={tabIndex === 0 ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
placeholder={tabIndex === MAP_TAB ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
value={search}
onChange={e => setSearch(() => e.target.value)}
tabIndex={-1}
@@ -213,31 +291,30 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<BsmSelect
className="bg-theme-1 rounded-full px-1 pb-0.5 text-center cursor-pointer"
options={sortOptions}
selected="name"
selected={selectedSort}
onChange={handleSortChange}
/>
<BsmButton
className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3"
iconClassName="transition-transform size-full ease-in-out duration-200 bg-light-main-color-1 dark:bg-main-color-3"
iconStyle={{ transform: mapSort.ascending ? "rotate(360deg)" : "rotate(180deg)" }}
iconStyle={{ transform: (tabIndex === MAP_TAB && mapSort.ascending)
|| (tabIndex === PLAYLIST_TAB && playlistSort.ascending)
? "rotate(360deg)" : "rotate(180deg)"}}
icon="chevron-top"
typeColor="primary"
withBar={false}
onClick={() => setMapSort({
compare: mapSort.compare,
ascending: !mapSort.ascending,
})}
onClick={handleAscendingClick}
/>
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} items={dropDownItems} menuTranslationY="6px" align="center" />
</nav>
<BsContentTabPanel
tabIndex={tabIndex}
onTabChange={index => setTabIndex(index)}
onTabChange={handleTabChange}
tabs={[
{
text: "misc.maps",
icon: MapIcon,
onClick: () => setTabIndex(0),
onClick: () => handleTabChange(MAP_TAB),
linkProps: version ? {
state: mapsLinkedState,
onClick: handleMapsLinkClick,
@@ -246,7 +323,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
{
text: "misc.playlists",
icon: PlaylistIcon,
onClick: () => setTabIndex(1),
onClick: () => handleTabChange(PLAYLIST_TAB),
linkProps: version ? {
state: playlistLinkedState,
onClick: handlePlaylistLinkClick,
@@ -292,7 +369,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
properties: ["openFile", "multiSelections"],
}}}
>
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
<LocalPlaylistsListPanel
className="w-full h-full shrink-0"
isActive={isActive && tabIndex === 1}
ref={playlistsRef}
version={version}
linkedState={playlistLinkedState}
filter={playlistFilter}
search={search}
sort={playlistSort}
/>
</Dropzone>
</InstalledMapsContext.Provider>
@@ -6,6 +6,8 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { hourToS, sToMs } from "shared/helpers/time.helpers";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import formatDuration from "format-duration";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { Comparator } from "shared/models/generics.type";
type Props = {
className?: string;
@@ -120,3 +122,8 @@ export type LocalPlaylistFilter = Partial<{
minDuration: number;
maxDuration: number;
}>
export interface LocalPlaylistSort {
compare: Comparator<LocalBPListsDetails>;
ascending: boolean;
}
@@ -24,7 +24,7 @@ import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playl
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
import { LocalPlaylistFilter } from "./local-playlist-filter-panel.component";
import { LocalPlaylistFilter, LocalPlaylistSort } from "./local-playlist-filter-panel.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
@@ -48,6 +48,7 @@ type Props = {
search?: string;
linkedState?: FolderLinkState;
isActive?: boolean;
sort: LocalPlaylistSort;
};
export type LocalPlaylistsListRef = {
@@ -57,7 +58,15 @@ export type LocalPlaylistsListRef = {
exportPlaylists: () => Promise<void>;
}
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({ version, className, filter: playlistFiler, search, isActive, linkedState }, forwardedRef) => {
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({
version,
className,
filter: playlistFilter,
search,
isActive,
linkedState,
sort: playlistSort,
}, forwardedRef) => {
const t = useTranslation();
@@ -397,27 +406,31 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(!p.playlistTitle?.toLowerCase().includes(search.toLowerCase())){ return false; }
if(!p.playlistAuthor?.toLowerCase().includes(search.toLowerCase())){ return false; }
if(typeof p.nbMaps === "number" && (typeof playlistFiler?.minNbMaps === "number" || typeof playlistFiler?.maxNbMaps === "number")){
if(playlistFiler?.minNbMaps && p.nbMaps < playlistFiler.minNbMaps){ return false; }
if(playlistFiler?.maxNbMaps && p.nbMaps > playlistFiler.maxNbMaps){ return false; }
if(typeof p.nbMaps === "number" && (typeof playlistFilter?.minNbMaps === "number" || typeof playlistFilter?.maxNbMaps === "number")){
if(playlistFilter?.minNbMaps && p.nbMaps < playlistFilter.minNbMaps){ return false; }
if(playlistFilter?.maxNbMaps && p.nbMaps > playlistFilter.maxNbMaps){ return false; }
}
if(typeof p.nbMappers === "number" && (typeof playlistFiler?.minNbMappers === "number" || typeof playlistFiler?.maxNbMappers === "number")){
if(playlistFiler?.minNbMappers && p.nbMappers < playlistFiler.minNbMappers){ return false; }
if(playlistFiler?.maxNbMappers && p.nbMappers > playlistFiler.maxNbMappers){ return false; }
if(typeof p.nbMappers === "number" && (typeof playlistFilter?.minNbMappers === "number" || typeof playlistFilter?.maxNbMappers === "number")){
if(playlistFilter?.minNbMappers && p.nbMappers < playlistFilter.minNbMappers){ return false; }
if(playlistFilter?.maxNbMappers && p.nbMappers > playlistFilter.maxNbMappers){ return false; }
}
if(typeof p.duration === "number" && (typeof playlistFiler?.minDuration === "number" || typeof playlistFiler?.maxDuration === "number")){
if(playlistFiler?.minDuration && p.duration < playlistFiler.minDuration){ return false; }
if(playlistFiler?.maxDuration && p.duration > playlistFiler.maxDuration){ return false; }
if(typeof p.duration === "number" && (typeof playlistFilter?.minDuration === "number" || typeof playlistFilter?.maxDuration === "number")){
if(playlistFilter?.minDuration && p.duration < playlistFilter.minDuration){ return false; }
if(playlistFilter?.maxDuration && p.duration > playlistFilter.maxDuration){ return false; }
}
if(typeof p.minNps === "number" && typeof playlistFiler.minNps === "number" && p.minNps < playlistFiler.minNps){ return false; }
if(typeof p.maxNps === "number" && typeof playlistFiler.maxNps === "number" && p.maxNps > playlistFiler.maxNps){ return false; }
if(typeof p.minNps === "number" && typeof playlistFilter.minNps === "number" && p.minNps < playlistFilter.minNps){ return false; }
if(typeof p.maxNps === "number" && typeof playlistFilter.maxNps === "number" && p.maxNps > playlistFilter.maxNps){ return false; }
return true;
});
}, [playlists, search, playlistFiler]);
})
.sort((playlist1, playlist2) =>
playlistSort.compare(playlist1, playlist2)
* (playlistSort.ascending ? 1 : -1)
);
}, [playlists, search, playlistFilter, playlistSort]);
return (
<div className={className}>
+13 -1
View File
@@ -49,17 +49,29 @@ export class MapsSorterService {
}
};
private addTiebreak(comparator: Comparator<BsmLocalMap>): Comparator<BsmLocalMap> {
return (map1, map2) =>
comparator(map1, map2)
|| this.getDefaultComparator()(map1, map2);
}
public getComparatorKeys(): string[] {
return Object.keys(this.comparators);
}
public getDefaultComparatorKey(): string {
return "name";
}
public getDefaultComparator(): Comparator<BsmLocalMap> {
return this.comparators.name;
}
public getComparator(key: string): Comparator<BsmLocalMap> {
const comparator = this.comparators[key];
return comparator || this.comparators.name;
return comparator
? this.addTiebreak(comparator)
: this.getDefaultComparator();
}
}
@@ -0,0 +1,74 @@
import { Comparator } from "shared/models/generics.type";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
export class PlaylistsSorterService {
private static instance: PlaylistsSorterService;
public static getInstance() {
if (!PlaylistsSorterService.instance) {
PlaylistsSorterService.instance = new PlaylistsSorterService();
}
return PlaylistsSorterService.instance;
}
private readonly comparators: {
[key: string]: Comparator<LocalBPListsDetails>;
} = {
title: (playlist1, playlist2) =>
playlist1.playlistTitle.localeCompare(playlist2.playlistTitle),
author: (playlist1, playlist2) =>
playlist1.playlistAuthor.localeCompare(playlist2.playlistAuthor),
"number-of-maps": (playlist1, playlist2) =>
playlist1.nbMaps - playlist2.nbMaps,
duration: (playlist1, playlist2) => {
if (!playlist1.duration) {
return playlist2.duration ? -1 : 0;
}
return !playlist2.duration ? -1
: playlist1.duration - playlist2.duration;
},
"min-notes-per-second": (playlist1, playlist2) => {
if (!playlist1.minNps) {
return playlist2.minNps ? -1 : 0;
}
return !playlist2.minNps ? -1
: playlist1.minNps - playlist2.minNps;
},
"max-notes-per-second": (playlist1, playlist2) => {
if (!playlist1.maxNps) {
return playlist2.maxNps ? -1 : 0;
}
return !playlist2.maxNps ? -1
: playlist1.maxNps - playlist2.maxNps;
},
};
private addTiebreak(comparator: Comparator<LocalBPListsDetails>): Comparator<LocalBPListsDetails> {
return (playlist1, playlist2) =>
comparator(playlist1, playlist2)
|| this.getDefaultComparator()(playlist1, playlist2);
}
public getComparatorKeys(): string[] {
return Object.keys(this.comparators);
}
public getDefaultComparatorKey(): string {
return "title";
}
public getDefaultComparator(): Comparator<LocalBPListsDetails> {
return this.comparators.title;
}
public getComparator(key: string): Comparator<LocalBPListsDetails> {
const comparator = this.comparators[key];
return comparator
? this.addTiebreak(comparator)
: this.getDefaultComparator();
}
}