diff --git a/assets/bsm_file.ico b/assets/bsm_file.ico new file mode 100644 index 00000000..48c48c62 Binary files /dev/null and b/assets/bsm_file.ico differ diff --git a/package.json b/package.json index b65c38c0..565697fd 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "owner": "Zagrios" }, "fileAssociations": [ - { "ext": "bplist", "description": "Beat Saber Playlist", "icon": "./assets/favicon.ico", "role": "Viewer" } + { "ext": "bplist", "description": "Beat Saber Playlist (BSManager)", "icon": "./assets/bsm_file.ico", "role": "Viewer" } ] }, "repository": { diff --git a/src/main/main.ts b/src/main/main.ts index 6cf82c74..c3d69279 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,6 +21,7 @@ import { BSLauncherService } from "./services/bs-launcher/bs-launcher.service"; import { IpcRequest } from "shared/models/ipc"; import { LivShortcut } from "./services/liv/liv-shortcut.service"; import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service"; +import { FileAssociationService } from "./services/file-association.service"; const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true"; @@ -69,19 +70,29 @@ const initServicesMustBeInitialized = () => { BSLauncherService.getInstance(); } +const findDeepLinkInArgs = (args: string[]): string => { + return args.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); +} + +const findAssociatedFileInArgs = (args: string[]): string => { + return args.find(arg => FileAssociationService.getInstance().isFileAssociated(arg)); +} + const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { app.on("second-instance", (_, argv) => { - const deepLink = argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); + const deepLink = findDeepLinkInArgs(argv); + const associatedFile = findAssociatedFileInArgs(argv); - if (!deepLink) { - return; + if (deepLink) { + DeepLinkService.getInstance().dispatchLinkOpened(deepLink); + } else if (associatedFile) { + FileAssociationService.getInstance().handleFileAssociation(associatedFile); } - DeepLinkService.getInstance().dispatchLinkOpened(deepLink); }); app.on("window-all-closed", () => { @@ -90,25 +101,28 @@ if (!gotTheLock) { }) app.whenReady().then(() => { - + app.setAppUserModelId(APP_NAME); initServicesMustBeInitialized(); - - const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); - if (!deepLink) { - createWindow(); - } else { + const deepLink = findDeepLinkInArgs(process.argv); + const associatedFile = findAssociatedFileInArgs(process.argv); + + if (deepLink) { DeepLinkService.getInstance().dispatchLinkOpened(deepLink); + } else if (associatedFile) { + FileAssociationService.getInstance().handleFileAssociation(associatedFile); + } else { + createWindow(); } - + SteamLauncherService.getInstance().restoreSteamVR(); - + // Log renderer errors ipcMain.on("log-error", (_, args: IpcRequest) => { log.error(args?.args); }); - + }).catch(log.error); } diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index 22881e54..a22d8e22 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -12,7 +12,7 @@ import { readFileSync } from "fs"; import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service"; import { copy, copyFile, pathExists, realpath } from "fs-extra"; import { Progression, ensureFolderExist, pathExist } from "../../helpers/fs.helpers"; -import { IpcService } from "../ipc.service"; +import { FileAssociationService } from "../file-association.service"; export class LocalPlaylistsManagerService { private static instance: LocalPlaylistsManagerService; @@ -33,18 +33,18 @@ export class LocalPlaylistsManagerService { private readonly maps: LocalMapsManagerService; private readonly request: RequestService; private readonly deepLink: DeepLinkService; + private readonly fileAssociation: FileAssociationService; private readonly windows: WindowManagerService; private readonly bsaver: BeatSaverService; - private readonly ipc: IpcService; private constructor() { this.maps = LocalMapsManagerService.getInstance(); this.versions = BSLocalVersionService.getInstance(); this.request = RequestService.getInstance(); this.deepLink = DeepLinkService.getInstance(); + this.fileAssociation = FileAssociationService.getInstance(); this.windows = WindowManagerService.getInstance(); this.bsaver = BeatSaverService.getInstance(); - this.ipc = IpcService.getInstance(); this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => { log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link); @@ -52,11 +52,16 @@ export class LocalPlaylistsManagerService { const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") : ""; this.openOneClickDownloadPlaylistWindow(bplistUrl); }); + + this.fileAssociation.registerFileAssociation(".bplist", filePath => { + log.info("FILE ASSOCIATION RECEIVED", filePath); + this.openOneClickDownloadPlaylistWindow(filePath); + }); } private async getPlaylistsFolder(version?: BSVersion) { if (!version) { - throw "Playlists are not available to be linked yet"; + throw new Error("Playlists are not available to be linked yet"); } const versionFolder = await this.versions.getVersionPath(version); @@ -79,14 +84,14 @@ export class LocalPlaylistsManagerService { } return lastValueFrom(this.request.downloadFile(bslistSource, destFile)).then(res => res.data); } - - private async readPlaylistFile(path: string): Promise { - if (!(await pathExist(path))) { - throw `bplist file not exist at ${path}`; + + private async readPlaylistFile(filePath: string): Promise { + if (!(await pathExist(filePath))) { + throw new Error(`bplist file not exist at ${filePath}`); } - const rawContent = readFileSync(path).toString(); + const rawContent = readFileSync(filePath).toString(); return JSON.parse(rawContent); } @@ -102,8 +107,8 @@ export class LocalPlaylistsManagerService { const bpListFilePath = await this.installBPListFile(bpListUrl, version); const bpList = await this.readPlaylistFile(bpListFilePath); - - const progress: Progression = { + + const progress: Progression = { total: bpList.songs.length, current: 0, data: { diff --git a/src/main/services/file-association.service.ts b/src/main/services/file-association.service.ts new file mode 100644 index 00000000..c43795e9 --- /dev/null +++ b/src/main/services/file-association.service.ts @@ -0,0 +1,56 @@ +import { pathExistsSync } from "fs-extra"; +import path from "path"; +import log from "electron-log"; + +export class FileAssociationService { + private static instance: FileAssociationService; + + public static getInstance(): FileAssociationService { + if (!FileAssociationService.instance) { + FileAssociationService.instance = new FileAssociationService(); + } + return FileAssociationService.instance; + } + + private readonly listeners = new Map(); + + private constructor() {} + + private getAbsolutePath(filePath: string) { + return path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); + } + + public registerFileAssociation(ext: ExtKey, fn: Listerner) { + if (!this.listeners.has(ext)) { + this.listeners.set(ext, [] as Listerner[]); + } + + this.listeners.get(ext).push(fn); + } + + public isFileAssociated(filePath: string): boolean { + const fileExt = path.extname(filePath) as ExtKey; + return this.listeners.has(fileExt); + } + + public handleFileAssociation(filePath: string) { + const absolutePath = this.getAbsolutePath(filePath); + + if(!pathExistsSync(absolutePath)) { + log.error(`[FileAssociationService] File not found: ${absolutePath}`); + return; + } + + const fileExt = path.extname(absolutePath) as ExtKey; + const listeners = this.listeners.get(fileExt); + + if (!listeners) { + log.error(`[FileAssociationService] No listeners for file: ${absolutePath}`); + return; + } + + listeners.forEach(listener => listener(this.getAbsolutePath(filePath))); + } +} +type ExtKey = `.${string}`; +type Listerner = (filePath: string) => void;