From bb9d95c0042b98058118579e9a9f152d595544ac Mon Sep 17 00:00:00 2001 From: Digimezzo Date: Sat, 4 Nov 2023 09:43:46 +0100 Subject: [PATCH 1/5] Adds working drag and drop service --- src/app/app.component.ts | 3 ++ src/app/app.module.ts | 3 ++ .../base-drag-and-drop.service.ts | 3 ++ .../drag-and-drop/drag-and-drop.service.ts | 30 +++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 src/app/services/drag-and-drop/base-drag-and-drop.service.ts create mode 100644 src/app/services/drag-and-drop/drag-and-drop.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e459b9ad8..c71d5dcb2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,6 +19,7 @@ import { BaseTranslatorService } from './services/translator/base-translator.ser import { BaseTrayService } from './services/tray/base-tray.service'; import { IntegrationTestRunner } from './testing/integration-test-runner'; import { AppConfig } from '../environments/environment'; +import {BaseDragAndDropService} from "./services/drag-and-drop/base-drag-and-drop.service"; @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -37,6 +38,7 @@ export class AppComponent implements OnInit { private trayService: BaseTrayService, private searchService: BaseSearchService, private mediaSessionService: BaseMediaSessionService, + private dragAndDropService: BaseDragAndDropService, private addToPlaylistMenu: AddToPlaylistMenu, private desktop: BaseDesktop, private logger: Logger, @@ -83,6 +85,7 @@ export class AppComponent implements OnInit { this.trayService.updateTrayContextMenu(); this.mediaSessionService.initialize(); this.scrobblingService.initialize(); + this.dragAndDropService.listenToOperatingSystemFileDrops(); await this.navigationService.navigateToLoadingAsync(); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4cf327225..b21333137 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -304,6 +304,8 @@ import { IntegrationTestRunner } from './testing/integration-test-runner'; import { AZLyricsApi } from './common/api/lyrics/a-z-lyrics-api'; import { WebSearchLyricsApi } from './common/api/lyrics/web-search-lyrics/web-search-lyrics-api'; import { WebSearchApi } from './common/api/lyrics/web-search-lyrics/web-search-api'; +import {BaseDragAndDropService} from "./services/drag-and-drop/base-drag-and-drop.service"; +import {DragAndDropService} from "./services/drag-and-drop/drag-and-drop.service"; export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -615,6 +617,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj { provide: BaseNowPlayingNavigationService, useClass: NowPlayingNavigationService }, { provide: BaseArtistInformationService, useClass: ArtistInformationService }, { provide: BaseLyricsService, useClass: LyricsService }, + { provide: BaseDragAndDropService, useClass: DragAndDropService }, { provide: ErrorHandler, useClass: GlobalErrorHandler, diff --git a/src/app/services/drag-and-drop/base-drag-and-drop.service.ts b/src/app/services/drag-and-drop/base-drag-and-drop.service.ts new file mode 100644 index 000000000..73f0c47af --- /dev/null +++ b/src/app/services/drag-and-drop/base-drag-and-drop.service.ts @@ -0,0 +1,3 @@ +export abstract class BaseDragAndDropService { + public abstract listenToOperatingSystemFileDrops(): void; +} diff --git a/src/app/services/drag-and-drop/drag-and-drop.service.ts b/src/app/services/drag-and-drop/drag-and-drop.service.ts new file mode 100644 index 000000000..c34df48d1 --- /dev/null +++ b/src/app/services/drag-and-drop/drag-and-drop.service.ts @@ -0,0 +1,30 @@ +import {Injectable} from "@angular/core"; +import {BaseDragAndDropService} from "./base-drag-and-drop.service"; + +@Injectable() +export class DragAndDropService implements BaseDragAndDropService{ + public listenToOperatingSystemFileDrops(): void { + document.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + + document.addEventListener('drop', (event) => { + event.preventDefault(); + event.stopPropagation(); + + alert('Dropped file(s)'); + + // let pathArr = []; + // for (const f of event.dataTransfer.files) { + // // Using the path attribute to get absolute file path + // console.log('File Path of dragged files: ', f.path) + // pathArr.push(f.path); // assemble array for main.js + // } + // console.log(pathArr); + // const ret = ipcRenderer.sendSync('dropped-file', pathArr); + // console.log(ret); + }); + } + +} \ No newline at end of file From 8864b477304651c364243140b93a1f71297d21d5 Mon Sep 17 00:00:00 2001 From: Digimezzo Date: Sat, 4 Nov 2023 13:58:35 +0100 Subject: [PATCH 2/5] Fixes Typescript error about iterables --- .../drag-and-drop/drag-and-drop.service.ts | 29 ++++++++----------- tsconfig.json | 2 +- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/app/services/drag-and-drop/drag-and-drop.service.ts b/src/app/services/drag-and-drop/drag-and-drop.service.ts index c34df48d1..5e6e3be07 100644 --- a/src/app/services/drag-and-drop/drag-and-drop.service.ts +++ b/src/app/services/drag-and-drop/drag-and-drop.service.ts @@ -1,30 +1,25 @@ -import {Injectable} from "@angular/core"; -import {BaseDragAndDropService} from "./base-drag-and-drop.service"; +import { Injectable } from '@angular/core'; +import { BaseDragAndDropService } from './base-drag-and-drop.service'; @Injectable() -export class DragAndDropService implements BaseDragAndDropService{ +export class DragAndDropService implements BaseDragAndDropService { public listenToOperatingSystemFileDrops(): void { document.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); }); - + document.addEventListener('drop', (event) => { event.preventDefault(); event.stopPropagation(); - - alert('Dropped file(s)'); - // let pathArr = []; - // for (const f of event.dataTransfer.files) { - // // Using the path attribute to get absolute file path - // console.log('File Path of dragged files: ', f.path) - // pathArr.push(f.path); // assemble array for main.js - // } - // console.log(pathArr); - // const ret = ipcRenderer.sendSync('dropped-file', pathArr); - // console.log(ret); + if (event.dataTransfer == undefined) { + return; + } + + for (const f of event.dataTransfer.files) { + alert(`Dropped file: ${f.path}`); + } }); } - -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index d0e9d773d..012ac1a00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "strictNullChecks": true, "target": "ES2022", "typeRoots": ["node_modules/@types"], - "lib": ["es2017", "es2016", "es2015", "dom"], + "lib": ["es2017", "es2016", "es2015", "dom", "DOM.Iterable"], "useDefineForClassFields": false, "skipLibCheck": true }, From f51aa7780aed43c1a787d3b4fa61a04ae40827e6 Mon Sep 17 00:00:00 2001 From: Digimezzo Date: Sat, 4 Nov 2023 16:25:59 +0100 Subject: [PATCH 3/5] Adds basic drop files support --- .../drag-and-drop.service.spec.ts | 28 +++++++++ .../drag-and-drop/drag-and-drop.service.ts | 10 ++- src/app/services/file/base-file.service.ts | 1 + src/app/services/file/file.service.spec.ts | 61 ++++++++++++++++--- src/app/services/file/file.service.ts | 6 +- 5 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 src/app/services/drag-and-drop/drag-and-drop.service.spec.ts diff --git a/src/app/services/drag-and-drop/drag-and-drop.service.spec.ts b/src/app/services/drag-and-drop/drag-and-drop.service.spec.ts new file mode 100644 index 000000000..983ecbc10 --- /dev/null +++ b/src/app/services/drag-and-drop/drag-and-drop.service.spec.ts @@ -0,0 +1,28 @@ +import { IMock, Mock } from 'typemoq'; +import { BaseFileService } from '../file/base-file.service'; +import { DragAndDropService } from './drag-and-drop.service'; +import { BaseDragAndDropService } from './base-drag-and-drop.service'; + +describe('DragAndDropService', () => { + let fileServiceMock: IMock; + + function createService(): BaseDragAndDropService { + return new DragAndDropService(fileServiceMock.object); + } + + beforeEach(() => { + fileServiceMock = Mock.ofType(); + }); + + describe('constructor', () => { + it('should create', () => { + // Arrange + + // Act + const sut: BaseDragAndDropService = createService(); + + // Assert + expect(sut).toBeDefined(); + }); + }); +}); diff --git a/src/app/services/drag-and-drop/drag-and-drop.service.ts b/src/app/services/drag-and-drop/drag-and-drop.service.ts index 5e6e3be07..ae8e19e75 100644 --- a/src/app/services/drag-and-drop/drag-and-drop.service.ts +++ b/src/app/services/drag-and-drop/drag-and-drop.service.ts @@ -1,8 +1,12 @@ import { Injectable } from '@angular/core'; import { BaseDragAndDropService } from './base-drag-and-drop.service'; +import { BaseFileService } from '../file/base-file.service'; +import { PromiseUtils } from '../../common/utils/promise-utils'; @Injectable() export class DragAndDropService implements BaseDragAndDropService { + public constructor(private fileService: BaseFileService) {} + public listenToOperatingSystemFileDrops(): void { document.addEventListener('dragover', (e) => { e.preventDefault(); @@ -17,9 +21,13 @@ export class DragAndDropService implements BaseDragAndDropService { return; } + const filePaths: string[] = []; + for (const f of event.dataTransfer.files) { - alert(`Dropped file: ${f.path}`); + filePaths.push(f.path); } + + PromiseUtils.noAwait(this.fileService.enqueueGivenParameterFilesAsync(filePaths)); }); } } diff --git a/src/app/services/file/base-file.service.ts b/src/app/services/file/base-file.service.ts index 666c676a9..88d648e2f 100644 --- a/src/app/services/file/base-file.service.ts +++ b/src/app/services/file/base-file.service.ts @@ -1,4 +1,5 @@ export abstract class BaseFileService { public abstract hasPlayableFilesAsParameters(): boolean; public abstract enqueueParameterFilesAsync(): Promise; + public abstract enqueueGivenParameterFilesAsync(parameters: string[]): Promise; } diff --git a/src/app/services/file/file.service.spec.ts b/src/app/services/file/file.service.spec.ts index 2e52e1161..08c2e3caf 100644 --- a/src/app/services/file/file.service.spec.ts +++ b/src/app/services/file/file.service.spec.ts @@ -33,7 +33,7 @@ describe('FileService', () => { trackModelFactoryMock.object, applicationMock.object, fileValidatorMock.object, - loggerMock.object + loggerMock.object, ); } @@ -92,10 +92,10 @@ describe('FileService', () => { x.enqueueAndPlayTracks( It.is( (trackModels: TrackModel[]) => - trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg' - ) + trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg', + ), ), - Times.once() + Times.once(), ); }); @@ -178,10 +178,10 @@ describe('FileService', () => { x.enqueueAndPlayTracks( It.is( (trackModels: TrackModel[]) => - trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg' - ) + trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg', + ), ), - Times.once() + Times.once(), ); }); @@ -209,4 +209,51 @@ describe('FileService', () => { playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); }); }); + + describe('enqueueGivenParameterFilesAsync', () => { + it('should enqueue all playable tracks found as parameters', async () => { + // Arrange + const parameterFiles: string[] = ['file 1.mp3', 'file 2.ogg', 'file 3.bmp']; + const service: BaseFileService = createService(); + + // Act + await service.enqueueGivenParameterFilesAsync(parameterFiles); + + // Assert + playbackServiceMock.verify( + (x) => + x.enqueueAndPlayTracks( + It.is( + (trackModels: TrackModel[]) => + trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg', + ), + ), + Times.once(), + ); + }); + + it('should not enqueue anything if parameters are empty', async () => { + // Arrange + const parameterFiles: string[] = []; + const service: BaseFileService = createService(); + + // Act + await service.enqueueGivenParameterFilesAsync(parameterFiles); + + // Assert + playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); + }); + + it('should not enqueue anything if there are no playable tracks found as parameters', async () => { + // Arrange + const parameterFiles: string[] = ['file 1.png', 'file 2.mkv', 'file 3.bmp']; + const service: BaseFileService = createService(); + + // Act + await service.enqueueGivenParameterFilesAsync(parameterFiles); + + // Assert + playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); + }); + }); }); diff --git a/src/app/services/file/file.service.ts b/src/app/services/file/file.service.ts index ec0acaf7f..df6b4a980 100644 --- a/src/app/services/file/file.service.ts +++ b/src/app/services/file/file.service.ts @@ -18,14 +18,14 @@ export class FileService implements BaseFileService { private trackModelFactory: TrackModelFactory, private application: BaseApplication, private fileValidator: FileValidator, - private logger: Logger + private logger: Logger, ) { this.subscription.add( this.application.argumentsReceived$.subscribe((argv: string[]) => { if (this.hasPlayableFilesAsGivenParameters(argv)) { PromiseUtils.noAwait(this.enqueueGivenParameterFilesAsync(argv)); } - }) + }), ); } @@ -61,7 +61,7 @@ export class FileService implements BaseFileService { return false; } - private async enqueueGivenParameterFilesAsync(parameters: string[]): Promise { + public async enqueueGivenParameterFilesAsync(parameters: string[]): Promise { const safeParameters: string[] = this.getSafeParameters(parameters); this.logger.info(`Found parameters: ${safeParameters.join(', ')}`, 'FileService', 'enqueueParameterFilesAsync'); From 8772d5fd9f5cb9fb34da9e99f0253b32a95ffaf5 Mon Sep 17 00:00:00 2001 From: Digimezzo Date: Sat, 4 Nov 2023 17:50:47 +0100 Subject: [PATCH 4/5] Some refactoring --- src/app/app.component.ts | 10 ++--- src/app/app.module.ts | 10 ++--- src/app/common/io/application.ts | 12 ------ src/app/common/io/base-application.ts | 1 - .../base-drag-and-drop.service.ts | 3 -- .../drag-and-drop.service.spec.ts | 28 ------------- .../drag-and-drop/drag-and-drop.service.ts | 33 ---------------- .../base-event-listener.service.ts | 7 ++++ .../event-listener.service.spec.ts | 20 ++++++++++ .../event-listener/event-listener.service.ts | 39 +++++++++++++++++++ src/app/services/file/base-file.service.ts | 1 - 11 files changed, 76 insertions(+), 88 deletions(-) delete mode 100644 src/app/services/drag-and-drop/base-drag-and-drop.service.ts delete mode 100644 src/app/services/drag-and-drop/drag-and-drop.service.spec.ts delete mode 100644 src/app/services/drag-and-drop/drag-and-drop.service.ts create mode 100644 src/app/services/event-listener/base-event-listener.service.ts create mode 100644 src/app/services/event-listener/event-listener.service.spec.ts create mode 100644 src/app/services/event-listener/event-listener.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c71d5dcb2..ee54cb7fd 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,7 +4,6 @@ import log from 'electron-log'; import * as path from 'path'; import { Subscription } from 'rxjs'; import { ProductInformation } from './common/application/product-information'; -import { BaseDesktop } from './common/io/base-desktop'; import { Logger } from './common/logger'; import { PromiseUtils } from './common/utils/promise-utils'; import { AddToPlaylistMenu } from './components/add-to-playlist-menu'; @@ -19,7 +18,9 @@ import { BaseTranslatorService } from './services/translator/base-translator.ser import { BaseTrayService } from './services/tray/base-tray.service'; import { IntegrationTestRunner } from './testing/integration-test-runner'; import { AppConfig } from '../environments/environment'; -import {BaseDragAndDropService} from "./services/drag-and-drop/base-drag-and-drop.service"; +import { BaseEventListenerService } from './services/event-listener/base-event-listener.service'; +import { BaseDesktop } from './common/io/base-desktop'; + @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -38,7 +39,7 @@ export class AppComponent implements OnInit { private trayService: BaseTrayService, private searchService: BaseSearchService, private mediaSessionService: BaseMediaSessionService, - private dragAndDropService: BaseDragAndDropService, + private eventListenerService: BaseEventListenerService, private addToPlaylistMenu: AddToPlaylistMenu, private desktop: BaseDesktop, private logger: Logger, @@ -85,8 +86,7 @@ export class AppComponent implements OnInit { this.trayService.updateTrayContextMenu(); this.mediaSessionService.initialize(); this.scrobblingService.initialize(); - this.dragAndDropService.listenToOperatingSystemFileDrops(); - + this.eventListenerService.listenToEvents(); await this.navigationService.navigateToLoadingAsync(); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b21333137..4f4626b59 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -56,13 +56,11 @@ import { Hacks } from './common/hacks'; import { ImageProcessor } from './common/image-processor'; import { Application } from './common/io/application'; import { BaseApplication } from './common/io/base-application'; -import { BaseDesktop } from './common/io/base-desktop'; import { BaseFileAccess } from './common/io/base-file-access'; import { BaseIpcProxy } from './common/io/base-ipc-proxy'; import { BaseMediaSessionProxy } from './common/io/base-media-session-proxy'; import { BaseTranslateServiceProxy } from './common/io/base-translate-service-proxy'; import { DateProxy } from './common/io/date-proxy'; -import { Desktop } from './common/io/desktop'; import { DocumentProxy } from './common/io/document-proxy'; import { FileAccess } from './common/io/file-access'; import { IpcProxy } from './common/io/ipc-proxy'; @@ -304,8 +302,10 @@ import { IntegrationTestRunner } from './testing/integration-test-runner'; import { AZLyricsApi } from './common/api/lyrics/a-z-lyrics-api'; import { WebSearchLyricsApi } from './common/api/lyrics/web-search-lyrics/web-search-lyrics-api'; import { WebSearchApi } from './common/api/lyrics/web-search-lyrics/web-search-api'; -import {BaseDragAndDropService} from "./services/drag-and-drop/base-drag-and-drop.service"; -import {DragAndDropService} from "./services/drag-and-drop/drag-and-drop.service"; +import { EventListenerService } from './services/event-listener/event-listener.service'; +import { BaseEventListenerService } from './services/event-listener/base-event-listener.service'; +import { BaseDesktop } from './common/io/base-desktop'; +import { Desktop } from './common/io/desktop'; export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -617,7 +617,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj { provide: BaseNowPlayingNavigationService, useClass: NowPlayingNavigationService }, { provide: BaseArtistInformationService, useClass: ArtistInformationService }, { provide: BaseLyricsService, useClass: LyricsService }, - { provide: BaseDragAndDropService, useClass: DragAndDropService }, + { provide: BaseEventListenerService, useClass: EventListenerService }, { provide: ErrorHandler, useClass: GlobalErrorHandler, diff --git a/src/app/common/io/application.ts b/src/app/common/io/application.ts index 34cd39c2c..bba4118ec 100644 --- a/src/app/common/io/application.ts +++ b/src/app/common/io/application.ts @@ -1,22 +1,10 @@ import { Injectable } from '@angular/core'; import * as remote from '@electron/remote'; -import { ipcRenderer } from 'electron'; -import { Observable, Subject } from 'rxjs'; import { BaseApplication } from './base-application'; import { WindowSize } from './window-size'; @Injectable() export class Application implements BaseApplication { - private argumentsReceived: Subject = new Subject(); - - public constructor() { - ipcRenderer.on('arguments-received', (event, argv: string[] | undefined) => { - this.argumentsReceived.next(argv); - }); - } - - public argumentsReceived$: Observable = this.argumentsReceived.asObservable(); - public getGlobal(name: string): unknown { return remote.getGlobal(name); } diff --git a/src/app/common/io/base-application.ts b/src/app/common/io/base-application.ts index dd7e040a5..6afb6e4f5 100644 --- a/src/app/common/io/base-application.ts +++ b/src/app/common/io/base-application.ts @@ -2,7 +2,6 @@ import { BrowserWindow } from 'electron'; import { Observable } from 'rxjs'; import { WindowSize } from './window-size'; export abstract class BaseApplication { - public abstract argumentsReceived$: Observable; public abstract getGlobal(name: string): unknown; public abstract getCurrentWindow(): BrowserWindow; public abstract getWindowSize(): WindowSize; diff --git a/src/app/services/drag-and-drop/base-drag-and-drop.service.ts b/src/app/services/drag-and-drop/base-drag-and-drop.service.ts deleted file mode 100644 index 73f0c47af..000000000 --- a/src/app/services/drag-and-drop/base-drag-and-drop.service.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class BaseDragAndDropService { - public abstract listenToOperatingSystemFileDrops(): void; -} diff --git a/src/app/services/drag-and-drop/drag-and-drop.service.spec.ts b/src/app/services/drag-and-drop/drag-and-drop.service.spec.ts deleted file mode 100644 index 983ecbc10..000000000 --- a/src/app/services/drag-and-drop/drag-and-drop.service.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IMock, Mock } from 'typemoq'; -import { BaseFileService } from '../file/base-file.service'; -import { DragAndDropService } from './drag-and-drop.service'; -import { BaseDragAndDropService } from './base-drag-and-drop.service'; - -describe('DragAndDropService', () => { - let fileServiceMock: IMock; - - function createService(): BaseDragAndDropService { - return new DragAndDropService(fileServiceMock.object); - } - - beforeEach(() => { - fileServiceMock = Mock.ofType(); - }); - - describe('constructor', () => { - it('should create', () => { - // Arrange - - // Act - const sut: BaseDragAndDropService = createService(); - - // Assert - expect(sut).toBeDefined(); - }); - }); -}); diff --git a/src/app/services/drag-and-drop/drag-and-drop.service.ts b/src/app/services/drag-and-drop/drag-and-drop.service.ts deleted file mode 100644 index ae8e19e75..000000000 --- a/src/app/services/drag-and-drop/drag-and-drop.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BaseDragAndDropService } from './base-drag-and-drop.service'; -import { BaseFileService } from '../file/base-file.service'; -import { PromiseUtils } from '../../common/utils/promise-utils'; - -@Injectable() -export class DragAndDropService implements BaseDragAndDropService { - public constructor(private fileService: BaseFileService) {} - - public listenToOperatingSystemFileDrops(): void { - document.addEventListener('dragover', (e) => { - e.preventDefault(); - e.stopPropagation(); - }); - - document.addEventListener('drop', (event) => { - event.preventDefault(); - event.stopPropagation(); - - if (event.dataTransfer == undefined) { - return; - } - - const filePaths: string[] = []; - - for (const f of event.dataTransfer.files) { - filePaths.push(f.path); - } - - PromiseUtils.noAwait(this.fileService.enqueueGivenParameterFilesAsync(filePaths)); - }); - } -} diff --git a/src/app/services/event-listener/base-event-listener.service.ts b/src/app/services/event-listener/base-event-listener.service.ts new file mode 100644 index 000000000..003183551 --- /dev/null +++ b/src/app/services/event-listener/base-event-listener.service.ts @@ -0,0 +1,7 @@ +import { Observable } from 'rxjs'; + +export abstract class BaseEventListenerService { + public abstract argumentsReceived$: Observable; + public abstract filesDropped$: Observable; + public abstract listenToEvents(): void; +} diff --git a/src/app/services/event-listener/event-listener.service.spec.ts b/src/app/services/event-listener/event-listener.service.spec.ts new file mode 100644 index 000000000..e247ef091 --- /dev/null +++ b/src/app/services/event-listener/event-listener.service.spec.ts @@ -0,0 +1,20 @@ +import { BaseEventListenerService } from './base-event-listener.service'; +import { EventListenerService } from './event-listener.service'; + +describe('EventListenerService', () => { + function createSut(): BaseEventListenerService { + return new EventListenerService(); + } + + describe('constructor', () => { + it('should create', () => { + // Arrange + + // Act + const sut: BaseEventListenerService = createSut(); + + // Assert + expect(sut).toBeDefined(); + }); + }); +}); diff --git a/src/app/services/event-listener/event-listener.service.ts b/src/app/services/event-listener/event-listener.service.ts new file mode 100644 index 000000000..6ea612739 --- /dev/null +++ b/src/app/services/event-listener/event-listener.service.ts @@ -0,0 +1,39 @@ +import { ipcRenderer } from 'electron'; +import { Observable, Subject } from 'rxjs'; +import { BaseEventListenerService } from './base-event-listener.service'; + +export class EventListenerService implements BaseEventListenerService { + private argumentsReceived: Subject = new Subject(); + private filesDropped: Subject = new Subject(); + + public argumentsReceived$: Observable = this.argumentsReceived.asObservable(); + public filesDropped$: Observable = this.filesDropped.asObservable(); + + public listenToEvents(): void { + ipcRenderer.on('arguments-received', (event, argv: string[] | undefined) => { + this.argumentsReceived.next(argv); + }); + + document.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + + document.addEventListener('drop', (event) => { + event.preventDefault(); + event.stopPropagation(); + + if (event.dataTransfer == undefined) { + return; + } + + const filePaths: string[] = []; + + for (const f of event.dataTransfer.files) { + filePaths.push(f.path); + } + + this.filesDropped.next(filePaths); + }); + } +} diff --git a/src/app/services/file/base-file.service.ts b/src/app/services/file/base-file.service.ts index 88d648e2f..666c676a9 100644 --- a/src/app/services/file/base-file.service.ts +++ b/src/app/services/file/base-file.service.ts @@ -1,5 +1,4 @@ export abstract class BaseFileService { public abstract hasPlayableFilesAsParameters(): boolean; public abstract enqueueParameterFilesAsync(): Promise; - public abstract enqueueGivenParameterFilesAsync(parameters: string[]): Promise; } From 1bd6a68d0de4e6ff309188c074674a123230f044 Mon Sep 17 00:00:00 2001 From: Digimezzo Date: Sat, 4 Nov 2023 18:04:47 +0100 Subject: [PATCH 5/5] Adds tests --- src/app/app.component.spec.ts | 4 ++ src/app/services/file/file.service.spec.ts | 81 +++++++++------------- src/app/services/file/file.service.ts | 12 +++- 3 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index d161d7305..978231c91 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -15,6 +15,7 @@ import { BaseSearchService } from './services/search/base-search.service'; import { BaseTranslatorService } from './services/translator/base-translator.service'; import { BaseTrayService } from './services/tray/base-tray.service'; import { IntegrationTestRunner } from './testing/integration-test-runner'; +import { BaseEventListenerService } from './services/event-listener/base-event-listener.service'; describe('AppComponent', () => { let navigationServiceMock: IMock; @@ -26,6 +27,7 @@ describe('AppComponent', () => { let trayServiceMock: IMock; let searchServiceMock: IMock; let mediaSessionServiceMock: IMock; + let eventListenerServiceMock: IMock; let addToPlaylistMenuMock: IMock; let desktopMock: IMock; @@ -48,6 +50,7 @@ describe('AppComponent', () => { trayServiceMock.object, searchServiceMock.object, mediaSessionServiceMock.object, + eventListenerServiceMock.object, addToPlaylistMenuMock.object, desktopMock.object, loggerMock.object, @@ -65,6 +68,7 @@ describe('AppComponent', () => { trayServiceMock = Mock.ofType(); searchServiceMock = Mock.ofType(); mediaSessionServiceMock = Mock.ofType(); + eventListenerServiceMock = Mock.ofType(); addToPlaylistMenuMock = Mock.ofType(); desktopMock = Mock.ofType(); loggerMock = Mock.ofType(); diff --git a/src/app/services/file/file.service.spec.ts b/src/app/services/file/file.service.spec.ts index 08c2e3caf..d36cf6434 100644 --- a/src/app/services/file/file.service.spec.ts +++ b/src/app/services/file/file.service.spec.ts @@ -11,9 +11,11 @@ import { TrackModelFactory } from '../track/track-model-factory'; import { BaseTranslatorService } from '../translator/base-translator.service'; import { BaseFileService } from './base-file.service'; import { FileService } from './file.service'; +import { BaseEventListenerService } from '../event-listener/base-event-listener.service'; describe('FileService', () => { let playbackServiceMock: IMock; + let eventListenerServiceMock: IMock; let trackModelFactoryMock: IMock; let fileValidatorMock: IMock; let applicationMock: IMock; @@ -25,11 +27,15 @@ describe('FileService', () => { let argumentsReceivedMock: Subject; let argumentsReceivedMock$: Observable; + let filesDroppedMock: Subject; + let filesDroppedMock$: Observable; + const flushPromises = () => new Promise(process.nextTick); function createService(): BaseFileService { return new FileService( playbackServiceMock.object, + eventListenerServiceMock.object, trackModelFactoryMock.object, applicationMock.object, fileValidatorMock.object, @@ -39,6 +45,7 @@ describe('FileService', () => { beforeEach(() => { playbackServiceMock = Mock.ofType(); + eventListenerServiceMock = Mock.ofType(); trackModelFactoryMock = Mock.ofType(); fileValidatorMock = Mock.ofType(); applicationMock = Mock.ofType(); @@ -64,7 +71,11 @@ describe('FileService', () => { argumentsReceivedMock = new Subject(); argumentsReceivedMock$ = argumentsReceivedMock.asObservable(); - applicationMock.setup((x) => x.argumentsReceived$).returns(() => argumentsReceivedMock$); + filesDroppedMock = new Subject(); + filesDroppedMock$ = filesDroppedMock.asObservable(); + + eventListenerServiceMock.setup((x) => x.argumentsReceived$).returns(() => argumentsReceivedMock$); + eventListenerServiceMock.setup((x) => x.filesDropped$).returns(() => filesDroppedMock$); }); describe('constructor', () => { @@ -134,6 +145,27 @@ describe('FileService', () => { // Assert playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); }); + + it('should enqueue all playable tracks that are dropped', async () => { + // Arrange + createService(); + + // Act + filesDroppedMock.next(['file 1.mp3', 'file 2.ogg', 'file 3.bmp']); + await flushPromises(); + + // Assert + playbackServiceMock.verify( + (x) => + x.enqueueAndPlayTracks( + It.is( + (trackModels: TrackModel[]) => + trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg', + ), + ), + Times.once(), + ); + }); }); describe('hasPlayableFilesAsParameters', () => { @@ -209,51 +241,4 @@ describe('FileService', () => { playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); }); }); - - describe('enqueueGivenParameterFilesAsync', () => { - it('should enqueue all playable tracks found as parameters', async () => { - // Arrange - const parameterFiles: string[] = ['file 1.mp3', 'file 2.ogg', 'file 3.bmp']; - const service: BaseFileService = createService(); - - // Act - await service.enqueueGivenParameterFilesAsync(parameterFiles); - - // Assert - playbackServiceMock.verify( - (x) => - x.enqueueAndPlayTracks( - It.is( - (trackModels: TrackModel[]) => - trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg', - ), - ), - Times.once(), - ); - }); - - it('should not enqueue anything if parameters are empty', async () => { - // Arrange - const parameterFiles: string[] = []; - const service: BaseFileService = createService(); - - // Act - await service.enqueueGivenParameterFilesAsync(parameterFiles); - - // Assert - playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); - }); - - it('should not enqueue anything if there are no playable tracks found as parameters', async () => { - // Arrange - const parameterFiles: string[] = ['file 1.png', 'file 2.mkv', 'file 3.bmp']; - const service: BaseFileService = createService(); - - // Act - await service.enqueueGivenParameterFilesAsync(parameterFiles); - - // Assert - playbackServiceMock.verify((x) => x.enqueueAndPlayTracks(It.isAny()), Times.never()); - }); - }); }); diff --git a/src/app/services/file/file.service.ts b/src/app/services/file/file.service.ts index df6b4a980..a715127c2 100644 --- a/src/app/services/file/file.service.ts +++ b/src/app/services/file/file.service.ts @@ -8,6 +8,7 @@ import { BasePlaybackService } from '../playback/base-playback.service'; import { TrackModel } from '../track/track-model'; import { TrackModelFactory } from '../track/track-model-factory'; import { BaseFileService } from './base-file.service'; +import { BaseEventListenerService } from '../event-listener/base-event-listener.service'; @Injectable() export class FileService implements BaseFileService { @@ -15,18 +16,25 @@ export class FileService implements BaseFileService { public constructor( private playbackService: BasePlaybackService, + private eventListenerService: BaseEventListenerService, private trackModelFactory: TrackModelFactory, private application: BaseApplication, private fileValidator: FileValidator, private logger: Logger, ) { this.subscription.add( - this.application.argumentsReceived$.subscribe((argv: string[]) => { + this.eventListenerService.argumentsReceived$.subscribe((argv: string[]) => { if (this.hasPlayableFilesAsGivenParameters(argv)) { PromiseUtils.noAwait(this.enqueueGivenParameterFilesAsync(argv)); } }), ); + + this.subscription.add( + this.eventListenerService.filesDropped$.subscribe((filePaths: string[]) => { + PromiseUtils.noAwait(this.enqueueGivenParameterFilesAsync(filePaths)); + }), + ); } public hasPlayableFilesAsParameters(): boolean { @@ -61,7 +69,7 @@ export class FileService implements BaseFileService { return false; } - public async enqueueGivenParameterFilesAsync(parameters: string[]): Promise { + private async enqueueGivenParameterFilesAsync(parameters: string[]): Promise { const safeParameters: string[] = this.getSafeParameters(parameters); this.logger.info(`Found parameters: ${safeParameters.join(', ')}`, 'FileService', 'enqueueParameterFilesAsync');