Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/file drag and drop #475

Merged
merged 5 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseNavigationService>;
Expand All @@ -26,6 +27,7 @@ describe('AppComponent', () => {
let trayServiceMock: IMock<BaseTrayService>;
let searchServiceMock: IMock<BaseSearchService>;
let mediaSessionServiceMock: IMock<BaseMediaSessionService>;
let eventListenerServiceMock: IMock<BaseEventListenerService>;

let addToPlaylistMenuMock: IMock<AddToPlaylistMenu>;
let desktopMock: IMock<BaseDesktop>;
Expand All @@ -48,6 +50,7 @@ describe('AppComponent', () => {
trayServiceMock.object,
searchServiceMock.object,
mediaSessionServiceMock.object,
eventListenerServiceMock.object,
addToPlaylistMenuMock.object,
desktopMock.object,
loggerMock.object,
Expand All @@ -65,6 +68,7 @@ describe('AppComponent', () => {
trayServiceMock = Mock.ofType<BaseTrayService>();
searchServiceMock = Mock.ofType<BaseSearchService>();
mediaSessionServiceMock = Mock.ofType<BaseMediaSessionService>();
eventListenerServiceMock = Mock.ofType<BaseEventListenerService>();
addToPlaylistMenuMock = Mock.ofType<AddToPlaylistMenu>();
desktopMock = Mock.ofType<BaseDesktop>();
loggerMock = Mock.ofType<Logger>();
Expand Down
7 changes: 5 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,6 +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 { BaseEventListenerService } from './services/event-listener/base-event-listener.service';
import { BaseDesktop } from './common/io/base-desktop';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
Expand All @@ -37,6 +39,7 @@ export class AppComponent implements OnInit {
private trayService: BaseTrayService,
private searchService: BaseSearchService,
private mediaSessionService: BaseMediaSessionService,
private eventListenerService: BaseEventListenerService,
private addToPlaylistMenu: AddToPlaylistMenu,
private desktop: BaseDesktop,
private logger: Logger,
Expand Down Expand Up @@ -83,7 +86,7 @@ export class AppComponent implements OnInit {
this.trayService.updateTrayContextMenu();
this.mediaSessionService.initialize();
this.scrobblingService.initialize();

this.eventListenerService.listenToEvents();
await this.navigationService.navigateToLoadingAsync();
}
}
7 changes: 5 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -304,6 +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 { 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');
Expand Down Expand Up @@ -615,6 +617,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
{ provide: BaseNowPlayingNavigationService, useClass: NowPlayingNavigationService },
{ provide: BaseArtistInformationService, useClass: ArtistInformationService },
{ provide: BaseLyricsService, useClass: LyricsService },
{ provide: BaseEventListenerService, useClass: EventListenerService },
{
provide: ErrorHandler,
useClass: GlobalErrorHandler,
Expand Down
12 changes: 0 additions & 12 deletions src/app/common/io/application.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> = new Subject();

public constructor() {
ipcRenderer.on('arguments-received', (event, argv: string[] | undefined) => {
this.argumentsReceived.next(argv);
});
}

public argumentsReceived$: Observable<string[]> = this.argumentsReceived.asObservable();

public getGlobal(name: string): unknown {
return remote.getGlobal(name);
}
Expand Down
1 change: 0 additions & 1 deletion src/app/common/io/base-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>;
public abstract getGlobal(name: string): unknown;
public abstract getCurrentWindow(): BrowserWindow;
public abstract getWindowSize(): WindowSize;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Observable } from 'rxjs';

export abstract class BaseEventListenerService {
public abstract argumentsReceived$: Observable<string[]>;
public abstract filesDropped$: Observable<string[]>;
public abstract listenToEvents(): void;
}
20 changes: 20 additions & 0 deletions src/app/services/event-listener/event-listener.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
39 changes: 39 additions & 0 deletions src/app/services/event-listener/event-listener.service.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> = new Subject();
private filesDropped: Subject<string[]> = new Subject();

public argumentsReceived$: Observable<string[]> = this.argumentsReceived.asObservable();
public filesDropped$: Observable<string[]> = 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);
});
}
}
48 changes: 40 additions & 8 deletions src/app/services/file/file.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BasePlaybackService>;
let eventListenerServiceMock: IMock<BaseEventListenerService>;
let trackModelFactoryMock: IMock<TrackModelFactory>;
let fileValidatorMock: IMock<FileValidator>;
let applicationMock: IMock<BaseApplication>;
Expand All @@ -25,20 +27,25 @@ describe('FileService', () => {
let argumentsReceivedMock: Subject<string[]>;
let argumentsReceivedMock$: Observable<string[]>;

let filesDroppedMock: Subject<string[]>;
let filesDroppedMock$: Observable<string[]>;

const flushPromises = () => new Promise(process.nextTick);

function createService(): BaseFileService {
return new FileService(
playbackServiceMock.object,
eventListenerServiceMock.object,
trackModelFactoryMock.object,
applicationMock.object,
fileValidatorMock.object,
loggerMock.object
loggerMock.object,
);
}

beforeEach(() => {
playbackServiceMock = Mock.ofType<BasePlaybackService>();
eventListenerServiceMock = Mock.ofType<BaseEventListenerService>();
trackModelFactoryMock = Mock.ofType<TrackModelFactory>();
fileValidatorMock = Mock.ofType<FileValidator>();
applicationMock = Mock.ofType<BaseApplication>();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -92,10 +103,10 @@ describe('FileService', () => {
x.enqueueAndPlayTracks(
It.is<TrackModel[]>(
(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(),
);
});

Expand Down Expand Up @@ -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<TrackModel[]>(
(trackModels: TrackModel[]) =>
trackModels.length === 2 && trackModels[0].path === 'file 1.mp3' && trackModels[1].path === 'file 2.ogg',
),
),
Times.once(),
);
});
});

describe('hasPlayableFilesAsParameters', () => {
Expand Down Expand Up @@ -178,10 +210,10 @@ describe('FileService', () => {
x.enqueueAndPlayTracks(
It.is<TrackModel[]>(
(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(),
);
});

Expand Down
14 changes: 11 additions & 3 deletions src/app/services/file/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,32 @@ 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 {
private subscription: Subscription = new Subscription();

public constructor(
private playbackService: BasePlaybackService,
private eventListenerService: BaseEventListenerService,
private trackModelFactory: TrackModelFactory,
private application: BaseApplication,
private fileValidator: FileValidator,
private logger: Logger
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));
}),
);
}

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
Loading