diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index a31eeca8..c4b3f3c6 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -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", diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index f332853c..8652ce7f 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -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", diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 3cb06f96..dcc44d63 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -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", diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 8624f473..6bfdaaf5 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -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", diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 213615ec..dcbd5e75 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -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": "インターネット接続がありません!", diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 8c576cc9..7603de71 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -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": "Нет интернета", diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 055926e5..83dbebc6 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -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": "無網路", diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 9b39cd49..5331c77f 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -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": "无网络", diff --git a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx index 26c9764b..b947889f 100644 --- a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx @@ -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({}); + const [playlistSort, setPlaylistSort] = useState({ + compare: playlistsSorter.getDefaultComparator(), + ascending: true, + }); + const [selectedSort, setSelectedSort] = useState("name"); + + const [sortOptions, setSortOptions] = useState[]>( + () => 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[] = 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) { setSearch(() => e.target.value)} tabIndex={-1} @@ -213,31 +291,30 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { setMapSort({ - compare: mapSort.compare, - ascending: !mapSort.ascending, - })} + onClick={handleAscendingClick} /> 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"], }}} > - + diff --git a/src/renderer/components/maps-playlists-panel/playlists/local-playlist-filter-panel.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/local-playlist-filter-panel.component.tsx index 7571c991..630b6e38 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/local-playlist-filter-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/local-playlist-filter-panel.component.tsx @@ -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; + ascending: boolean; +} diff --git a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx index 094adee9..96138052 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx @@ -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; } -export const LocalPlaylistsListPanel = forwardRef(({ version, className, filter: playlistFiler, search, isActive, linkedState }, forwardedRef) => { +export const LocalPlaylistsListPanel = forwardRef(({ + version, + className, + filter: playlistFilter, + search, + isActive, + linkedState, + sort: playlistSort, +}, forwardedRef) => { const t = useTranslation(); @@ -397,27 +406,31 @@ export const LocalPlaylistsListPanel = forwardRef( 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 (
diff --git a/src/renderer/services/maps/sorter.service.ts b/src/renderer/services/maps/sorter.service.ts index 14330b40..bcf759c4 100644 --- a/src/renderer/services/maps/sorter.service.ts +++ b/src/renderer/services/maps/sorter.service.ts @@ -49,17 +49,29 @@ export class MapsSorterService { } }; + private addTiebreak(comparator: Comparator): Comparator { + 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 { return this.comparators.name; } public getComparator(key: string): Comparator { const comparator = this.comparators[key]; - return comparator || this.comparators.name; + return comparator + ? this.addTiebreak(comparator) + : this.getDefaultComparator(); } } diff --git a/src/renderer/services/playlists/sorter.service.ts b/src/renderer/services/playlists/sorter.service.ts new file mode 100644 index 00000000..a194be6b --- /dev/null +++ b/src/renderer/services/playlists/sorter.service.ts @@ -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; + } = { + 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): Comparator { + 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 { + return this.comparators.title; + } + + public getComparator(key: string): Comparator { + const comparator = this.comparators[key]; + return comparator + ? this.addTiebreak(comparator) + : this.getDefaultComparator(); + } + +}