From d390050183b9a2618ed43a38c10d919495eaa85f Mon Sep 17 00:00:00 2001 From: MathieuG-P Date: Sat, 26 Nov 2022 23:29:50 +0100 Subject: [PATCH] can now link and unlink version maps --- .eslintrc.js | 3 +- package-lock.json | 28 ++++----- package.json | 1 + src/main/ipcs/bs-maps-ipcs.ts | 27 ++++++++- .../maps/local-maps-manager.service.ts | 57 +++++++++++++------ src/main/services/utils.service.ts | 15 +++++ .../local-maps-list-panel.component.tsx | 9 ++- .../maps-playlists-panel.component.tsx | 36 ++++++++++-- .../shared/tab-nav-bar.component.tsx | 4 +- .../beat-saver/beat-saver-api.service.ts | 13 ++++- src/renderer/services/maps-manager.service.ts | 29 ++++++++-- 11 files changed, 171 insertions(+), 51 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9e335203..86e6ab35 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,7 +32,8 @@ module.exports = { "@typescript-eslint/no-throw-literal": "off", "@typescript-eslint/no-unused-expressions": "off", "no-param-reassign": "off", - "no-useless-escape": "off" + "no-useless-escape": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off" }, parserOptions: { ecmaVersion: 2020, diff --git a/package-lock.json b/package-lock.json index 4202eb0a..425672fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1876,7 +1876,7 @@ }, "@types/archiver": { "version": "5.3.1", - "resolved": "https://npm.zone51.tf/@types%2farchiver/-/archiver-5.3.1.tgz", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.1.tgz", "integrity": "sha512-wKYZaSXaDvTZuInAWjCeGG7BEAgTWG2zZW0/f7IYFcoHB2X2d9lkVFnrOlXl3W6NrvO6Ml3FLLu8Uksyymcpnw==", "dev": true, "requires": { @@ -2396,7 +2396,7 @@ }, "@types/yargs": { "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types%2fyargs/-/yargs-17.0.13.tgz", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", "dev": true, "requires": { @@ -2998,7 +2998,7 @@ }, "archiver": { "version": "5.3.1", - "resolved": "https://npm.zone51.tf/archiver/-/archiver-5.3.1.tgz", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", "requires": { "archiver-utils": "^2.1.0", @@ -3012,7 +3012,7 @@ }, "archiver-utils": { "version": "2.1.0", - "resolved": "https://npm.zone51.tf/archiver-utils/-/archiver-utils-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "requires": { "glob": "^7.1.4", @@ -4053,7 +4053,7 @@ }, "compress-commons": { "version": "4.1.1", - "resolved": "https://npm.zone51.tf/compress-commons/-/compress-commons-4.1.1.tgz", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", "requires": { "buffer-crc32": "^0.2.13", @@ -4281,12 +4281,12 @@ }, "crc-32": { "version": "1.2.2", - "resolved": "https://npm.zone51.tf/crc-32/-/crc-32-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" }, "crc32-stream": { "version": "4.0.2", - "resolved": "https://npm.zone51.tf/crc32-stream/-/crc32-stream-4.0.2.tgz", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", "requires": { "crc-32": "^1.2.0", @@ -6895,7 +6895,7 @@ }, "fs-constants": { "version": "1.0.0", - "resolved": "https://npm.zone51.tf/fs-constants/-/fs-constants-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { @@ -9447,7 +9447,7 @@ }, "lazystream": { "version": "1.0.1", - "resolved": "https://npm.zone51.tf/lazystream/-/lazystream-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "requires": { "readable-stream": "^2.0.5" @@ -11763,7 +11763,7 @@ }, "readdir-glob": { "version": "1.1.2", - "resolved": "https://npm.zone51.tf/readdir-glob/-/readdir-glob-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.2.tgz", "integrity": "sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==", "requires": { "minimatch": "^5.1.0" @@ -11771,7 +11771,7 @@ "dependencies": { "brace-expansion": { "version": "2.0.1", - "resolved": "https://npm.zone51.tf/brace-expansion/-/brace-expansion-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "requires": { "balanced-match": "^1.0.0" @@ -11779,7 +11779,7 @@ }, "minimatch": { "version": "5.1.0", - "resolved": "https://npm.zone51.tf/minimatch/-/minimatch-5.1.0.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "requires": { "brace-expansion": "^2.0.1" @@ -12945,7 +12945,7 @@ }, "tar-stream": { "version": "2.2.0", - "resolved": "https://npm.zone51.tf/tar-stream/-/tar-stream-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "requires": { "bl": "^4.0.3", @@ -14135,7 +14135,7 @@ }, "zip-stream": { "version": "4.1.0", - "resolved": "https://npm.zone51.tf/zip-stream/-/zip-stream-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", "requires": { "archiver-utils": "^2.1.0", diff --git a/package.json b/package.json index ed308e56..4f9ccf0b 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "electron-store": "^8.1.0", "electron-updater": "^5.2.4", "framer-motion": "^7.2.1", + "fs-extra": "^10.1.0", "history": "^5.3.0", "is-online": "^9.0.1", "md5-file": "^5.0.0", diff --git a/src/main/ipcs/bs-maps-ipcs.ts b/src/main/ipcs/bs-maps-ipcs.ts index 6cb53976..22d95292 100644 --- a/src/main/ipcs/bs-maps-ipcs.ts +++ b/src/main/ipcs/bs-maps-ipcs.ts @@ -21,7 +21,30 @@ ipcMain.on("verion-have-maps-linked", async (event, request: IpcRequest(request.responceChannel, {success: true, data: await maps.versionIsLinked(request.args)}); - }); \ No newline at end of file + }); + +ipcMain.on("link-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.linkVersionMaps(request.args.version, request.args.keepMaps).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: true, error: err}); + }); + +}); + +ipcMain.on("unlink-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.unlinkVersionMaps(request.args.version, request.args.keepMaps).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: true, error: err}); + }); + +}); \ No newline at end of file diff --git a/src/main/services/maps/local-maps-manager.service.ts b/src/main/services/maps/local-maps-manager.service.ts index 35453b20..d7ddf9b1 100644 --- a/src/main/services/maps/local-maps-manager.service.ts +++ b/src/main/services/maps/local-maps-manager.service.ts @@ -6,7 +6,8 @@ import { BSLocalVersionService } from "../bs-local-version.service"; import { InstallationLocationService } from "../installation-location.service"; import { UtilsService } from "../utils.service"; import crypto from "crypto"; -import { lstat, lstatSync, symlinkSync } from "fs"; +import { lstat, lstatSync, symlinkSync, readdir, fstat, unlinkSync } from "fs"; +import { copySync } from "fs-extra"; export class LocalMapsManagerService { @@ -32,7 +33,11 @@ export class LocalMapsManagerService { private async getMapsFolderPath(version?: BSVersion): Promise{ if(version){ return path.join(await this.localVersion.getVersionPath(version), this.LEVELS_ROOT_FOLDER, this.CUSTOM_LEVELS_FOLDER); } - return path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER); + const sharedMapsPath = path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER); + if(!(await this.utils.pathExist(sharedMapsPath))){ + this.utils.createFolderIfNotExist(sharedMapsPath); + } + return sharedMapsPath; } private async computeMapHash(mapPath: string, rawInfoString: string): Promise{ @@ -42,7 +47,7 @@ export class LocalMapsManagerService { for(const diff of set._difficultyBeatmaps){ const diffFilePath = path.join(mapPath, diff._beatmapFilename); if(!await this.utils.pathExist(diffFilePath)){ continue; } - const diffContent = (await this.utils.readFileAsync(diffFilePath)).toString() + const diffContent = (await this.utils.readFileAsync(diffFilePath)).toString(); content += diffContent; } } @@ -69,27 +74,19 @@ export class LocalMapsManagerService { } public async getMaps(version?: BSVersion): Promise{ - const levelsFolder = await this.getMapsFolderPath(version) + const levelsFolder = await this.getMapsFolderPath(version); const levelsPath = (await this.utils.pathExist(levelsFolder)) ? this.utils.listDirsInDir(levelsFolder, true) : []; - const mapsInfo = await Promise.all(levelsPath.map(levelPath => this.loadMapInfoFromPath(levelPath))) - - console.log(mapsInfo); + const mapsInfo = await Promise.all(levelsPath.map(levelPath => this.loadMapInfoFromPath(levelPath))); return mapsInfo.filter(info => !!info); } - public deleteMap(version?: BSVersion){ - - } - public async versionIsLinked(version: BSVersion): Promise{ const levelsPath = await this.getMapsFolderPath(version); - console.log(levelsPath, version); - const isPathExist = await this.utils.pathExist(levelsPath); if(!isPathExist){ return false; } @@ -97,13 +94,37 @@ export class LocalMapsManagerService { return lstatSync(levelsPath).isSymbolicLink() } - public async linkVersionMaps(verion: BSVersion, includeVersionMaps: boolean): Promise{ - //TODO - //Can use fs.symlink to create symlink + public async linkVersionMaps(version: BSVersion, keepMaps: boolean): Promise{ + + if(await this.versionIsLinked(version)){ return; } + + const sharedMapsPath = await this.getMapsFolderPath(); + const versionMapsPath = await this.getMapsFolderPath(version); + + if(keepMaps){ + await this.utils.moveDirContent(versionMapsPath, sharedMapsPath); + } + + await this.utils.deleteFolder(versionMapsPath); + + symlinkSync(sharedMapsPath, versionMapsPath, "junction"); } - public async unlinkVersionMaps(version: BSVersion, keepLinkedMaps: boolean): Promise{ - //TODO + public async unlinkVersionMaps(version: BSVersion, keepMaps: boolean): Promise{ + + const sharedMapsPath = await this.getMapsFolderPath(); + const versionMapsPath = await this.getMapsFolderPath(version); + + if(await this.versionIsLinked(version)){ + unlinkSync(versionMapsPath); + } + + this.utils.createFolderIfNotExist(versionMapsPath); + + if(keepMaps){ + copySync(sharedMapsPath, versionMapsPath); + } + } diff --git a/src/main/services/utils.service.ts b/src/main/services/utils.service.ts index 62600de9..d0f77d5b 100644 --- a/src/main/services/utils.service.ts +++ b/src/main/services/utils.service.ts @@ -1,4 +1,5 @@ import { existsSync, mkdirSync, readdirSync, readFile, unlinkSync } from "fs"; +import { moveSync } from "fs-extra" import { spawnSync } from "child_process"; import { homedir } from "os"; import path from "path"; @@ -75,9 +76,23 @@ export class UtilsService{ } public deleteFolder(folderPath: string): Promise{ + const folderExist = this.pathExist(folderPath); + if(!folderExist){ return; } return rm(folderPath, {recursive: true}); } + public async moveDirContent(src: string, dest: string, overwrite = false): Promise{ + const [srcExist, destExist] = await Promise.all([this.pathExist(src), this.pathExist(dest)]); + if(!srcExist){ return; } + if(!destExist){ await this.createFolderIfNotExist(dest); } + readdirSync(src, {encoding: "utf-8"}).forEach(file => { + const srcFullPath = path.join(src, file); + const destFullPath = path.join(dest, file); + if(!overwrite && this.pathExist(destFullPath)){ return; } + moveSync(srcFullPath, destFullPath, {overwrite}); + }); + } + public ipcSend(channel: string, response: IpcResponse): void{ try { this.mainWindow.webContents.send(channel, response); diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx index 234c381b..6dc25489 100644 --- a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -27,9 +27,16 @@ export function LocalMapsListPanel({version, className, filter, search} : Props) const subs: Subscription[] = []; - if(isVisible){ + const loadMaps = () => { + console.log("LOAD MAPS"); subs.push(mapsManager.getMaps(version).subscribe(localMaps => setMaps(() => [...localMaps]))); } + + if(isVisible){ + loadMaps(); + subs.push(mapsManager.versionLinked$.subscribe(loadMaps)); + subs.push(mapsManager.versionUnlinked$.subscribe(loadMaps)); + } return () => { setMaps(() => []); diff --git a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx index 19825fd1..767aee75 100644 --- a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx @@ -8,7 +8,7 @@ import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model" import { BsmButton } from "../shared/bsm-button.component" import { useThemeColor } from "renderer/hooks/use-theme-color.hook" import { MapsManagerService } from "renderer/services/maps-manager.service" -import { motion } from "framer-motion"; +import { motion, Variants } from "framer-motion"; type Props = { oneBlock?: boolean, @@ -31,9 +31,12 @@ export function MapsPlaylistsPanel({version, oneBlock = false}: Props) { useEffect(() => { + loadMapIsLinked(); + }, [version]); + + const loadMapIsLinked = () => { mapsService.versionHaveMapsLinked(version).then(setMapsLinked); - }, [version]) - + } const handleSearch = (value: string) => { if(tabIndex === 0){ @@ -42,16 +45,37 @@ export function MapsPlaylistsPanel({version, oneBlock = false}: Props) { return setPlaylistSearch(() => value); } - const renderTab = (props: DetailedHTMLProps, any>, text: string): JSX.Element => { + const handleMapsLinkClick = () => { + if(!mapsLinked){ + return mapsService.linkVersion(version).then(loadMapIsLinked); + } + mapsService.unlinkVersion(version).then(loadMapIsLinked); + } + + const handlePlaylistLinkClick = () => { + + } + + const renderTab = (props: DetailedHTMLProps, HTMLLIElement>, text: string, index: number): JSX.Element => { const mainColor = mapsLinked ? color : "red"; + const variants: Variants = { + hover: {rotate: 22.5}, + tap: {rotate: 45} + } + + const onClickLink = (index: number) => { + if(index === 0){ return handleMapsLinkClick() }; + handlePlaylistLinkClick(); + } + return (
  • {text} - + - + {e.stopPropagation(); onClickLink(index)}}/>
  • ) diff --git a/src/renderer/components/shared/tab-nav-bar.component.tsx b/src/renderer/components/shared/tab-nav-bar.component.tsx index 8edaa13d..4907d3cb 100644 --- a/src/renderer/components/shared/tab-nav-bar.component.tsx +++ b/src/renderer/components/shared/tab-nav-bar.component.tsx @@ -6,7 +6,7 @@ type Props = { tabsText: string[], onTabChange: (index: number) => void, className?: string, - renderTab?: (props: DetailedHTMLProps, HTMLDivElement>, text: string) => JSX.Element + renderTab?: (props: DetailedHTMLProps, any>, text: string, index?: number) => JSX.Element } export function TabNavBar(props: Props) { @@ -30,7 +30,7 @@ export function TabNavBar(props: Props) { {props.tabsText.map((text, index) => ( props.renderTab ? ( - {props.renderTab({onClick: () => selectTab(index)}, t(text))} + {props.renderTab({onClick: () => selectTab(index)}, t(text), index)} ) : (
  • selectTab(index)}>{t(text)}
  • diff --git a/src/renderer/services/beat-saver/beat-saver-api.service.ts b/src/renderer/services/beat-saver/beat-saver-api.service.ts index 0eeb32be..7f15410b 100644 --- a/src/renderer/services/beat-saver/beat-saver-api.service.ts +++ b/src/renderer/services/beat-saver/beat-saver-api.service.ts @@ -22,9 +22,18 @@ export class BeatSaverApiService { const resp = await fetch(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`); - const data = await resp.json() as Record, BsvMapDetail>; + const data = await resp.json() as Record, BsvMapDetail> | BsvMapDetail; - return {status: resp.status, data: data}; + if((data as BsvMapDetail).id){ + const key = (data as BsvMapDetail).versions.at(0).hash.toLowerCase(); + const parsedData = { + [key]: data as BsvMapDetail + } as Record, BsvMapDetail>; + + return {status: resp.status, data: parsedData} + } + + return {status: resp.status, data: (data as Record, BsvMapDetail>)}; } diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index ebe988b8..a7c7f672 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -1,4 +1,4 @@ -import { Observable } from "rxjs"; +import { Subject, Observable } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { BeatSaverService } from "./beat-saver/beat-saver.service"; @@ -16,6 +16,9 @@ export class MapsManagerService { private readonly ipcService: IpcService; private readonly bsaver: BeatSaverService; + private readonly lastLinkedVersion$: Subject = new Subject(); + private readonly lastUnlinkedVersion$: Subject = new Subject(); + private constructor(){ this.ipcService = IpcService.getInstance(); this.bsaver = BeatSaverService.getInstance(); @@ -44,14 +47,30 @@ export class MapsManagerService { }) } - public downloadMap(map: any, version?: BSVersion){ - + public linkVersion(version: BSVersion): Promise{ + return this.ipcService.send("link-version-maps", {args: {version, keepMaps: true}}).then(res => { + if(res.success){ + return this.lastLinkedVersion$.next(version); + } + throw res.error; + }); } - public deleteMaps(maps: any[], version?: BSVersion){ - + public unlinkVersion(version: BSVersion): Promise{ + return this.ipcService.send("unlink-version-maps", {args: {version, keepMaps: true}}).then(res => { + if(res.success){ + return this.lastUnlinkedVersion$.next(version); + } + throw res.error; + }); } + public get versionLinked$(): Observable{ + return this.lastLinkedVersion$.asObservable(); + } + public get versionUnlinked$(): Observable{ + return this.lastUnlinkedVersion$.asObservable(); + } } \ No newline at end of file