diff --git a/api.ts b/api.ts index cea7fab1e..61f720f22 100644 --- a/api.ts +++ b/api.ts @@ -18,6 +18,7 @@ import { PLAYLIST_PARSE_RESPONSE, PLAYLIST_UPDATE, PLAYLIST_UPDATE_RESPONSE, + PLAYLIST_UPDATE_POSITIONS, } from './shared/ipc-commands'; const fs = require('fs'); @@ -205,6 +206,17 @@ export class Api { } ); + ipcMain.on( + PLAYLIST_UPDATE_POSITIONS, + (event, playlists: Partial) => + playlists.forEach((list, index) => { + this.updatePlaylistById(list._id, { + ...list, + position: index, + }); + }) + ); + this.refreshPlaylists(); } @@ -261,8 +273,9 @@ export class Api { autoRefresh: 1, updateDate: 1, updateState: 1, + position: 1, }) - .sort({ importDate: -1 }); + .sort({ position: 1, importDate: -1 }); } /** diff --git a/shared/ipc-commands.ts b/shared/ipc-commands.ts index 7a618d5a8..66c01a56b 100644 --- a/shared/ipc-commands.ts +++ b/shared/ipc-commands.ts @@ -11,6 +11,7 @@ export const PLAYLIST_PARSE = 'PLAYLIST:PARSE_PLAYLIST'; export const PLAYLIST_PARSE_RESPONSE = 'PLAYLIST:PARSE_PLAYLIST_RESPONSE'; export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE'; export const PLAYLIST_UPDATE_RESPONSE = 'PLAYLIST:UPDATE_RESPONSE'; +export const PLAYLIST_UPDATE_POSITIONS = 'PLAYLIST:UPDATE_POSITIONS'; // General export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW'; diff --git a/shared/playlist.interface.ts b/shared/playlist.interface.ts index 98ede0f9d..b0002ec04 100644 --- a/shared/playlist.interface.ts +++ b/shared/playlist.interface.ts @@ -28,4 +28,5 @@ export interface Playlist { autoRefresh: boolean; updateDate?: number; updateState?: PlaylistUpdateState; + position?: number; } diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 0eede4020..971f2def0 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -28,6 +28,7 @@ export type PlaylistMeta = Pick< | 'filePath' | 'updateDate' | 'updateState' + | 'position' >; @Component({ diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index 01f2672eb..38bc06f51 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -8,9 +8,16 @@ import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.co import { HomeRoutingModule } from './home.routing'; import { NgxUploaderModule } from 'ngx-uploader'; import { PlaylistInfoComponent } from './recent-playlists/playlist-info/playlist-info.component'; +import { DragDropModule } from '@angular/cdk/drag-drop'; @NgModule({ - imports: [CommonModule, HomeRoutingModule, NgxUploaderModule, SharedModule], + imports: [ + CommonModule, + HomeRoutingModule, + NgxUploaderModule, + SharedModule, + DragDropModule, + ], declarations: [ HomeComponent, FileUploadComponent, diff --git a/src/app/home/recent-playlists/recent-playlists.component.html b/src/app/home/recent-playlists/recent-playlists.component.html index 1696995f0..62511b4d0 100644 --- a/src/app/home/recent-playlists/recent-playlists.component.html +++ b/src/app/home/recent-playlists/recent-playlists.component.html @@ -1,4 +1,4 @@ - + cloud
{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}
@@ -6,11 +6,14 @@ {{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
- + drag_indicator delete - +
diff --git a/src/app/home/recent-playlists/recent-playlists.component.scss b/src/app/home/recent-playlists/recent-playlists.component.scss index 0532a0869..895bcb14a 100644 --- a/src/app/home/recent-playlists/recent-playlists.component.scss +++ b/src/app/home/recent-playlists/recent-playlists.component.scss @@ -9,6 +9,7 @@ mat-list-item { mat-nav-list { height: calc(100vh - 192px); overflow: auto; + width: 100%; } .meta { @@ -16,3 +17,43 @@ mat-nav-list { color: #666; margin-top: 2px !important; } + +::ng-deep .cdk-drag-preview .mat-list-item-content { + display: flex; + flex-direction: row; + align-items: center; + box-sizing: border-box; + padding: 0 16px; + position: relative; + height: inherit; + width: 100%; + justify-content: space-between; + + .mat-list-text { + display: flex; + flex-direction: column; + flex: auto; + box-sizing: border-box; + overflow: hidden; + padding-left: 16px; + } +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.drag-icon { + cursor: move; + margin-left: -20px; +} diff --git a/src/app/home/recent-playlists/recent-playlists.component.spec.ts b/src/app/home/recent-playlists/recent-playlists.component.spec.ts new file mode 100644 index 000000000..ae1e69e2f --- /dev/null +++ b/src/app/home/recent-playlists/recent-playlists.component.spec.ts @@ -0,0 +1,78 @@ +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { MockModule, MockPipe, MockProvider } from 'ng-mocks'; +import { ElectronService } from '../../services/electron.service'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { RecentPlaylistsComponent } from './recent-playlists.component'; +import { PlaylistMeta } from '../home.component'; +import { MatListModule } from '@angular/material/list'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; + +export class ElectronServiceStub { + sendIpcEvent(): void {} +} + +describe('RecentPlaylistsComponent', () => { + let component: RecentPlaylistsComponent; + let fixture: ComponentFixture; + let electronService: ElectronService; + let dialog: MatDialog; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + RecentPlaylistsComponent, + MockPipe(TranslatePipe), + ], + imports: [ + MockModule(MatDialogModule), + MockModule(MatListModule), + MockModule(MatIconModule), + MockModule(MatTooltipModule), + ], + providers: [ + { provide: ElectronService, useClass: ElectronServiceStub }, + MockProvider(TranslateService), + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(RecentPlaylistsComponent); + component = fixture.componentInstance; + component.playlists = []; + dialog = TestBed.inject(MatDialog); + electronService = TestBed.inject(ElectronService); + TestBed.inject(ElectronService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should open the info dialog', () => { + spyOn(dialog, 'open'); + component.openInfoDialog({} as PlaylistMeta); + expect(dialog.open).toHaveBeenCalledTimes(1); + }); + + it('should send an ipc event after drop event', () => { + const event = { + previousIndex: 0, + currentIndex: 1, + item: undefined, + container: undefined, + previousContainer: undefined, + isPointerOverContainer: true, + distance: { x: 0, y: 0 }, + dropPoint: { x: 0, y: 0 }, + }; + spyOn(electronService, 'sendIpcEvent'); + component.drop(event); + expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/home/recent-playlists/recent-playlists.component.ts b/src/app/home/recent-playlists/recent-playlists.component.ts index f499e0e55..520eea343 100644 --- a/src/app/home/recent-playlists/recent-playlists.component.ts +++ b/src/app/home/recent-playlists/recent-playlists.component.ts @@ -1,7 +1,10 @@ +import { PLAYLIST_UPDATE_POSITIONS } from './../../../../shared/ipc-commands'; import { Component, Input, Output, EventEmitter } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { PlaylistInfoComponent } from './playlist-info/playlist-info.component'; import { PlaylistMeta } from './../home.component'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { ElectronService } from '../../services/electron.service'; @Component({ selector: 'app-recent-playlists', @@ -24,8 +27,12 @@ export class RecentPlaylistsComponent { /** * Creates an instance of the component * @param dialog angular material dialog reference + * @param electronService electron service */ - constructor(public dialog: MatDialog) {} + constructor( + public dialog: MatDialog, + private electronService: ElectronService + ) {} /** * Opens the details dialog with the information about the provided playlist @@ -36,4 +43,20 @@ export class RecentPlaylistsComponent { data, }); } + + /** + * Drop event handler - applies the new sort order to the playlists array + * @param event drop event + */ + drop(event: CdkDragDrop): void { + moveItemInArray( + this.playlists, + event.previousIndex, + event.currentIndex + ); + this.electronService.sendIpcEvent( + PLAYLIST_UPDATE_POSITIONS, + this.playlists + ); + } } diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index 9eb506f42..d6309ce43 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -37,4 +37,13 @@ export class ElectronService { getAppVersion(): string { return this.remote.app.getVersion(); } + + /** + * Sends an IPC event from render to the main process + * @param type event type + * @param payload data payload + */ + sendIpcEvent(type: string, payload: unknown): void { + this.ipcRenderer.send(type, payload); + } }