mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #601 from silentrald/feat/216
[feat-216] support for sorting maps/songs and playlists
This commit is contained in:
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Deine Karten importieren",
|
||||
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Name",
|
||||
"song-author": "Liedautor",
|
||||
"map-author": "Kartenautor",
|
||||
"bpm": "BPM",
|
||||
"duration": "Dauer",
|
||||
"likes": "Likes",
|
||||
"date-uploaded": "Hochladedatum"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importiere deine Wiedergabelisten",
|
||||
"subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Titel",
|
||||
"author": "Autor",
|
||||
"number-of-maps": "Anzahl der Karten",
|
||||
"duration": "Dauer",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Import your maps",
|
||||
"subtext": "Drop your zip files here to import your maps"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Name",
|
||||
"song-author": "Song Author",
|
||||
"map-author": "Map Author",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duration",
|
||||
"likes": "Likes",
|
||||
"date-uploaded": "Date Uploaded"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Import your playlists",
|
||||
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Title",
|
||||
"author": "Author",
|
||||
"number-of-maps": "Number of Maps",
|
||||
"duration": "Duration",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Importar tus mapas",
|
||||
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Nombre",
|
||||
"song-author": "Autor de la canción",
|
||||
"map-author": "Autor del mapa",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duración",
|
||||
"likes": "Me gusta",
|
||||
"date-uploaded": "Fecha de subida"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importa tus listas de reproducción",
|
||||
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Título",
|
||||
"author": "Autor",
|
||||
"number-of-maps": "Número de mapas",
|
||||
"duration": "Duración",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Importer vos maps",
|
||||
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Nom",
|
||||
"song-author": "Auteur de la chanson",
|
||||
"map-author": "Auteur de la carte",
|
||||
"bpm": "BPM",
|
||||
"duration": "Durée",
|
||||
"likes": "J'aime",
|
||||
"date-uploaded": "Date de téléchargement"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importer vos playlists",
|
||||
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Titre",
|
||||
"author": "Auteur",
|
||||
"number-of-maps": "Nombre de cartes",
|
||||
"duration": "Durée",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Importa le tue mappe",
|
||||
"subtext": "Rilascia qui i tuoi file zip per importare le tue mappe"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Nome",
|
||||
"song-author": "Autore della canzone",
|
||||
"map-author": "Autore della mappa",
|
||||
"bpm": "BPM",
|
||||
"duration": "Durata",
|
||||
"likes": "Mi piace",
|
||||
"date-uploaded": "Data di caricamento"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importa le tue playlist",
|
||||
"subtext": "Rilascia qui i file \".bplist\" o \".json\" per importarli"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Titolo",
|
||||
"author": "Autore",
|
||||
"number-of-maps": "Numero di mappe",
|
||||
"duration": "Durata",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "マップをインポート",
|
||||
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
|
||||
},
|
||||
"sort": {
|
||||
"name": "名前",
|
||||
"song-author": "曲の作者",
|
||||
"map-author": "地図の作者",
|
||||
"bpm": "BPM",
|
||||
"duration": "持続時間",
|
||||
"likes": "いいね",
|
||||
"date-uploaded": "アップロード日"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "プレイリストをインポート",
|
||||
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
|
||||
},
|
||||
"sort": {
|
||||
"title": "タイトル",
|
||||
"author": "著者",
|
||||
"number-of-maps": "地図の数",
|
||||
"duration": "持続時間",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "맵 가져오기",
|
||||
"subtext": "ZIP 파일을 여기에 드롭하여 맵 가져오기"
|
||||
},
|
||||
"sort": {
|
||||
"name": "이름",
|
||||
"song-author": "작곡가",
|
||||
"map-author": "지도 제작자",
|
||||
"bpm": "BPM",
|
||||
"duration": "지속 시간",
|
||||
"likes": "좋아요",
|
||||
"date-uploaded": "업로드 날짜"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "재생 목록 가져오기",
|
||||
"subtext": "\".bplist\" 또는 \".json\" 파일을 여기에 드롭하여 가져오기"
|
||||
},
|
||||
"sort": {
|
||||
"title": "제목",
|
||||
"author": "저자",
|
||||
"number-of-maps": "지도 수",
|
||||
"duration": "지속 시간",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте свои карты",
|
||||
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Имя",
|
||||
"song-author": "Автор песни",
|
||||
"map-author": "Автор карты",
|
||||
"bpm": "BPM",
|
||||
"duration": "Длительность",
|
||||
"likes": "Лайки",
|
||||
"date-uploaded": "Дата загрузки"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте ваши плейлисты",
|
||||
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Заголовок",
|
||||
"author": "Автор",
|
||||
"number-of-maps": "Количество карт",
|
||||
"duration": "Длительность",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "導入您的地圖",
|
||||
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
|
||||
},
|
||||
"sort": {
|
||||
"name": "名字",
|
||||
"song-author": "歌曲作者",
|
||||
"map-author": "地圖作者",
|
||||
"bpm": "BPM",
|
||||
"duration": "持續時間",
|
||||
"likes": "喜歡",
|
||||
"date-uploaded": "上傳日期"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "匯入您的播放列表",
|
||||
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
|
||||
},
|
||||
"sort": {
|
||||
"title": "標題",
|
||||
"author": "作者",
|
||||
"number-of-maps": "地圖數量",
|
||||
"duration": "持續時間",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"drop-zone": {
|
||||
"text": "导入您的地图",
|
||||
"subtext": "将ZIP文件拖放到此处以导入您的地图"
|
||||
},
|
||||
"sort": {
|
||||
"name": "名字",
|
||||
"song-author": "歌曲作者",
|
||||
"map-author": "地图作者",
|
||||
"bpm": "BPM",
|
||||
"duration": "持续时间",
|
||||
"likes": "喜欢",
|
||||
"date-uploaded": "上传日期"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +111,13 @@
|
||||
"drop-zone": {
|
||||
"text": "导入您的播放列表",
|
||||
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
|
||||
},
|
||||
"sort": {
|
||||
"title": "标题",
|
||||
"author": "作者",
|
||||
"number-of-maps": "地图数量",
|
||||
"duration": "持续时间",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ jest.mock("electron-log", () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
jest.mock("ps-list", () => () => []);
|
||||
jest.mock("ps-list", () => (): unknown[] => []);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const IS_LINUX = process.platform === "linux";
|
||||
|
||||
@@ -3,10 +3,10 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsListPanel, LocalMapsListPanelRef } from "./maps/local-maps-list-panel.component";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
import { FilterPanel } from "./maps/filter-panel.component";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapFilter, MapSort } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
|
||||
@@ -20,10 +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 { BsmButton } from "../shared/bsm-button.component";
|
||||
import { mapSorter } from "shared/models/maps/sorter";
|
||||
import { playlistSorter } from "shared/models/playlists/sorter";
|
||||
|
||||
type Props = {
|
||||
readonly version?: BSVersion;
|
||||
@@ -39,12 +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 playlistsManager = useService(PlaylistsManagerService);
|
||||
const playlistsDownloader = useService(PlaylistDownloaderService);
|
||||
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const [mapsDropZoneOpen, setMapsDropZoneOpen] = useState(false);
|
||||
@@ -64,7 +71,23 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const playlistsRef = useRef<LocalPlaylistsListRef>();
|
||||
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
const [mapSort, setMapSort] = useState<MapSort>({
|
||||
compare: mapSorter.getDefaultComparator(),
|
||||
ascending: true,
|
||||
});
|
||||
const [playlistFilter, setPlaylistFilter] = useState<LocalPlaylistFilter>({});
|
||||
const [playlistSort, setPlaylistSort] = useState<LocalPlaylistSort>({
|
||||
compare: playlistSorter.getDefaultComparator(),
|
||||
ascending: true,
|
||||
});
|
||||
const [selectedSort, setSelectedSort] = useState<string>("name");
|
||||
|
||||
const [sortOptions, setSortOptions] = useState<BsmSelectOption<string>[]>(
|
||||
() => mapSorter.getComparatorKeys().map(key => ({
|
||||
text: `pages.version-viewer.maps.tabs.maps.sort.${key}`,
|
||||
value: key,
|
||||
}))
|
||||
);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const mapsLinkedState = useObservable(() => {
|
||||
@@ -100,6 +123,42 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return mapsManager.unlinkVersion(version);
|
||||
};
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
if (tabIndex === index) {
|
||||
return;
|
||||
}
|
||||
setTabIndex(index);
|
||||
|
||||
switch (index) {
|
||||
case MAP_TAB:
|
||||
setSortOptions(() => mapSorter.getComparatorKeys().map(key => ({
|
||||
text: `pages.version-viewer.maps.tabs.maps.sort.${key}`,
|
||||
value: key,
|
||||
})));
|
||||
setSelectedSort(mapSorter.defaultKey);
|
||||
setMapSort({
|
||||
compare: mapSorter.getDefaultComparator(),
|
||||
ascending: playlistSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
case PLAYLIST_TAB:
|
||||
setSortOptions(() => playlistSorter.getComparatorKeys().map(key => ({
|
||||
text: `pages.version-viewer.maps.tabs.playlists.sort.${key}`,
|
||||
value: key,
|
||||
})));
|
||||
setSelectedSort(playlistSorter.defaultKey);
|
||||
setPlaylistSort({
|
||||
compare: playlistSorter.getDefaultComparator(),
|
||||
ascending: mapSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlaylistLinkClick = () => {
|
||||
if(playlistLinkedState === FolderLinkState.Pending || playlistLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
|
||||
|
||||
@@ -135,8 +194,50 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
];
|
||||
})();
|
||||
|
||||
const handleSortChange = (key: string) => {
|
||||
switch (tabIndex) {
|
||||
case MAP_TAB:
|
||||
setMapSort({
|
||||
compare: mapSorter.getComparator(key),
|
||||
ascending: mapSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
case PLAYLIST_TAB:
|
||||
setPlaylistSort({
|
||||
compare: playlistSorter.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?.() },
|
||||
@@ -153,7 +254,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center gap-2 text-main-color-1 dark:text-white mb-4">
|
||||
<BsmDropdownButton
|
||||
classNames={{
|
||||
mainContainer: "h-full relative z-[1] flex justify-center w-fit",
|
||||
@@ -164,7 +265,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
icon="add"
|
||||
buttonColor="primary"
|
||||
text="misc.add"
|
||||
align="center"
|
||||
align="left"
|
||||
menuTranslationY="6px"
|
||||
withBar={false}
|
||||
items={addDropDownItems}
|
||||
@@ -173,7 +274,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}
|
||||
@@ -188,16 +289,37 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
)
|
||||
)}
|
||||
</BsmDropdownButton>
|
||||
<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" />
|
||||
|
||||
<div className="h-full flex bg-theme-2 rounded-full">
|
||||
<BsmButton
|
||||
className="h-full aspect-square rounded-l-full z-[1]"
|
||||
iconClassName="transition-transform size-full ease-in-out duration-200 bg-theme-2"
|
||||
iconStyle={{ transform: (tabIndex === MAP_TAB && mapSort.ascending)
|
||||
|| (tabIndex === PLAYLIST_TAB && playlistSort.ascending)
|
||||
? "rotate(0deg)" : "rotate(180deg)"}}
|
||||
icon="arrow-upward"
|
||||
withBar={false}
|
||||
onClick={handleAscendingClick}
|
||||
/>
|
||||
|
||||
<BsmSelect
|
||||
className="h-full bg-theme-2 rounded-r-full cursor-pointer outline-none pb-1 text-center"
|
||||
options={sortOptions}
|
||||
selected={selectedSort}
|
||||
onChange={handleSortChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="right" />
|
||||
</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,
|
||||
@@ -206,7 +328,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,
|
||||
@@ -228,7 +350,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
|
||||
<LocalMapsListPanel
|
||||
className="w-full h-full shrink-0"
|
||||
isActive={isActive && tabIndex === 0}
|
||||
ref={mapsRef}
|
||||
version={version}
|
||||
filter={mapFilter}
|
||||
search={search}
|
||||
linkedState={mapsLinkedState}
|
||||
sort={mapSort}
|
||||
/>
|
||||
</Dropzone>
|
||||
|
||||
<Dropzone
|
||||
@@ -243,7 +374,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>
|
||||
|
||||
+14
-3
@@ -3,7 +3,7 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapFilter, MapSort } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { bufferTime, last, tap } from "rxjs/operators";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
@@ -32,6 +32,7 @@ type Props = {
|
||||
search?: string;
|
||||
linkedState?: FolderLinkState;
|
||||
isActive?: boolean;
|
||||
sort: MapSort;
|
||||
};
|
||||
|
||||
export type LocalMapsListPanelRef = {
|
||||
@@ -40,7 +41,15 @@ export type LocalMapsListPanelRef = {
|
||||
removeDuplicates: () => void;
|
||||
}
|
||||
|
||||
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
|
||||
export const LocalMapsListPanel = forwardRef<unknown, Props>(({
|
||||
version,
|
||||
className,
|
||||
filter,
|
||||
search,
|
||||
linkedState,
|
||||
isActive,
|
||||
sort,
|
||||
}, forwardRef) => {
|
||||
const mapsManager = useService(MapsManagerService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
|
||||
@@ -204,7 +213,9 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
|
||||
}, [version]);
|
||||
|
||||
const preppedMaps: RenderableMap[] = (() => {
|
||||
return renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? [];
|
||||
return (renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? [])
|
||||
// NOTE: might be good to cache sorted maps, to reduce loading
|
||||
.sort((map1, map2) => sort.compare(map1.map, map2.map) * (sort.ascending === false ? -1 : 1));
|
||||
})();
|
||||
|
||||
if (!maps) {
|
||||
|
||||
+7
@@ -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/comparator.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;
|
||||
}
|
||||
|
||||
+28
-15
@@ -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}>
|
||||
|
||||
@@ -133,7 +133,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
|
||||
|
||||
return (
|
||||
<div ref={ref} className="w-full h-full flex flex-col items-center justify-center gap-4">
|
||||
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
|
||||
<div className="w-full shrink-0 flex h-9 justify-center gap-2 text-main-color-1 dark:text-white">
|
||||
<BsmButton
|
||||
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold"
|
||||
icon="add"
|
||||
@@ -149,7 +149,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
|
||||
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
|
||||
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={t("models.panel.actions.search")} onChange={e => setSearch(e.target.value)} tabIndex={-1} />
|
||||
</div>
|
||||
<BsmDropdownButton items={threeDotsItems} 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} menuTranslationY="6px" align="center" />
|
||||
<BsmDropdownButton items={threeDotsItems} 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} menuTranslationY="6px" align="right" />
|
||||
</div>
|
||||
<BsContentTabPanel
|
||||
tabIndex={currentTabIndex}
|
||||
|
||||
@@ -68,10 +68,11 @@ import { BrowseIcon } from "./icons/browse-icon.component";
|
||||
import { AddFileIcon } from "./icons/add-file-icon.component";
|
||||
import { CancelIcon } from "./icons/cancel-icon.component";
|
||||
import { WarningIcon } from "./icons/warning-icon.component";
|
||||
import { ArrowUpwardIcon } from "./icons/arrow-upward-icon.component";
|
||||
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
|
||||
|
||||
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
@@ -282,6 +283,10 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
if(icon === "add-file") {
|
||||
return <AddFileIcon className={className} style={style} />;
|
||||
}
|
||||
if(icon === "arrow-upward") {
|
||||
return <ArrowUpwardIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if(icon === "null") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const ArrowUpwardIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M440-608 324-492q-11 11-28 11t-28-11q-11-11-11-28t11-28l184-184q12-12 28-12t28 12l184 184q11 11 11 28t-11 28q-11 11-28 11t-28-11L520-608v328q0 17-11.5 28.5T480-240q-17 0-28.5-11.5T440-280v-328Z" />
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export enum Comparison {
|
||||
EQUAL = 0,
|
||||
GREATER = 1,
|
||||
LESSER = -1,
|
||||
};
|
||||
|
||||
export type Comparator<T> = (object1: T, object2: T) => number;
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ObjectValues } from "shared/helpers/type.helpers";
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
|
||||
import { BsmLocalMap } from "./bsm-local-map.interface";
|
||||
import { Comparator } from "../comparator.type";
|
||||
|
||||
export interface BsvMapDetail {
|
||||
automapper: boolean;
|
||||
@@ -250,6 +252,11 @@ export interface MapFilter {
|
||||
excludedTags?: Set<MapTag>;
|
||||
}
|
||||
|
||||
export interface MapSort {
|
||||
compare: Comparator<BsmLocalMap>;
|
||||
ascending: boolean;
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
docs: BsvMapDetail[];
|
||||
redirect: string;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Comparison } from "../comparator.type";
|
||||
import { Sorter } from "../sorter.model";
|
||||
import { BsmLocalMap } from "./bsm-local-map.interface";
|
||||
|
||||
const sortName = (map1: BsmLocalMap, map2: BsmLocalMap) => map1.mapInfo.songName.localeCompare(map2.mapInfo.songName);
|
||||
|
||||
export const mapSorter = new Sorter<BsmLocalMap>({
|
||||
comparators: {
|
||||
name: sortName,
|
||||
"song-author": (map1, map2) => map1.mapInfo.songAuthorName.localeCompare(map2.mapInfo.songAuthorName),
|
||||
"map-author": (map1, map2) => {
|
||||
// Compare the number of mappers, else compare the first mapper
|
||||
const difference = map1.mapInfo.levelMappers.length - map2.mapInfo.levelMappers.length;
|
||||
return difference || map1.mapInfo.levelMappers[0].localeCompare(map2.mapInfo.levelMappers[0]);
|
||||
},
|
||||
bpm: (map1, map2) => map1.mapInfo.beatsPerMinute - map2.mapInfo.beatsPerMinute,
|
||||
duration: (map1, map2) => {
|
||||
if (!map1.songDetails) {
|
||||
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
|
||||
}
|
||||
|
||||
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.duration - map2.songDetails.duration;
|
||||
},
|
||||
likes: (map1, map2) => {
|
||||
if (!map1.songDetails) {
|
||||
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
|
||||
}
|
||||
|
||||
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.upVotes - map2.songDetails.upVotes;
|
||||
},
|
||||
"date-uploaded": (map1, map2) => {
|
||||
if (!map1.songDetails) {
|
||||
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
|
||||
}
|
||||
|
||||
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.uploadedAt - map2.songDetails.uploadedAt;
|
||||
},
|
||||
},
|
||||
tiebreak: sortName,
|
||||
defaultKey: "name"
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Comparison } from "../comparator.type";
|
||||
import { Sorter } from "../sorter.model";
|
||||
import { LocalBPListsDetails } from "./local-playlist.models";
|
||||
|
||||
const sortTitle = (playlist1: LocalBPListsDetails, playlist2: LocalBPListsDetails) =>
|
||||
playlist1.playlistTitle.localeCompare(playlist2.playlistTitle);
|
||||
|
||||
export const playlistSorter = new Sorter({
|
||||
comparators: {
|
||||
title: sortTitle,
|
||||
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 ? Comparison.LESSER : Comparison.EQUAL;
|
||||
}
|
||||
|
||||
return !playlist2.duration ? Comparison.GREATER : playlist1.duration - playlist2.duration;
|
||||
},
|
||||
"notes-per-second": (playlist1, playlist2) => {
|
||||
if (!playlist1.maxNps) {
|
||||
return playlist2.maxNps ? Comparison.LESSER : Comparison.EQUAL;
|
||||
}
|
||||
|
||||
return !playlist2.maxNps ? Comparison.GREATER : playlist1.maxNps - playlist2.maxNps;
|
||||
},
|
||||
},
|
||||
tiebreak: sortTitle,
|
||||
defaultKey: "title",
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Comparator } from "./comparator.type";
|
||||
|
||||
export type SorterOptions<T> = {
|
||||
defaultKey: string;
|
||||
comparators: Record<string, Comparator<T>>;
|
||||
tiebreak: Comparator<T>;
|
||||
}
|
||||
|
||||
export class Sorter<T> {
|
||||
public readonly defaultKey: string;
|
||||
|
||||
private readonly comparators: Record<string, Comparator<T>>;
|
||||
private readonly tiebreak: Comparator<T>;
|
||||
|
||||
constructor(options: SorterOptions<T>) {
|
||||
this.defaultKey = options.defaultKey;
|
||||
this.comparators = options.comparators;
|
||||
this.tiebreak = options.tiebreak;
|
||||
}
|
||||
|
||||
public getComparatorKeys(): string[] {
|
||||
return Object.keys(this.comparators);
|
||||
}
|
||||
|
||||
public getDefaultComparator(): Comparator<T> {
|
||||
return this.comparators[this.defaultKey];
|
||||
}
|
||||
|
||||
public getComparator(key: string): Comparator<T> {
|
||||
const comparator = this.comparators[key] || this.comparators[this.defaultKey];
|
||||
return this.tiebreak === comparator
|
||||
? comparator
|
||||
: (object1, object2) => comparator(object1, object2) || this.tiebreak(object1, object2);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user