Unified create and modify playlist components

This commit is contained in:
Isaac Abadi
2022-06-20 16:02:15 -04:00
parent 7f47fb339b
commit e1cb56e8e9
7 changed files with 237 additions and 73 deletions

View File

@@ -45,6 +45,9 @@ export class CustomPlaylistsComponent implements OnInit {
// creating a playlist
openCreatePlaylistDialog(): void {
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: {
create_mode: true
},
minWidth: '90vw',
minHeight: '95vh'
});
@@ -103,9 +106,10 @@ export class CustomPlaylistsComponent implements OnInit {
editPlaylistDialog(args: { playlist: Playlist; index: number; }): void {
const playlist = args.playlist;
const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: {
playlist_id: playlist.id
playlist_id: playlist.id,
create_mode: false
},
minWidth: '85vw'
});
@@ -113,7 +117,7 @@ export class CustomPlaylistsComponent implements OnInit {
dialogRef.afterClosed().subscribe(() => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlists[index] = dialogRef.componentInstance.original_playlist;
this.playlists[index] = dialogRef.componentInstance.playlist;
}
});
}

View File

@@ -48,31 +48,53 @@
</div>
<div *ngIf="selectMode">
<mat-selection-list *ngIf="normal_files_received" (selectionChange)="fileSelectionChanged($event)">
<mat-list-option [selected]="selected_data.includes(file.uid)" *ngFor="let file of paged_data" [value]="file.uid">
<div class="container">
<div class="row justify-content-center">
<div class="col-10">
<mat-icon class="audio-video-icon">{{(file.type === 'audio' || file.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
{{file.title}}
</div>
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
</div>
<mat-tab-group [(selectedIndex)]="selectedIndex">
<mat-tab label="Order" i18n-label="Order">
<div *ngIf="selected_data.length">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</mat-list-option>
</mat-selection-list>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<mat-selection-list *ngIf="!normal_files_received">
<mat-list-option *ngFor="let file of paged_data">
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" width="250" height="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option>
</mat-selection-list>
</ng-container>
<!-- Selection order -->
<mat-button-toggle-group *ngIf="selected_data.length" class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<div style="margin-top: 20px;" *ngIf="!selected_data.length">
<h4 style="text-align: center;">No files selected!</h4>
</div>
</mat-tab>
<mat-tab label="Select files" i18n-label="Select files">
<mat-selection-list *ngIf="normal_files_received" (selectionChange)="fileSelectionChanged($event)">
<mat-list-option [selected]="selected_data.includes(file.uid)" *ngFor="let file of paged_data" [value]="file">
<div class="container">
<div class="row justify-content-center">
<div class="col-10">
<mat-icon class="audio-video-icon">{{(file.type === 'audio' || file.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
{{file.title}}
</div>
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
</div>
</div>
</mat-list-option>
</mat-selection-list>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<mat-selection-list *ngIf="!normal_files_received">
<mat-list-option *ngFor="let file of paged_data">
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" width="250" height="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option>
</mat-selection-list>
</ng-container>
</mat-tab>
</mat-tab-group>
</div>
<div *ngIf="usePaginator">
<div style="position: relative;" *ngIf="usePaginator && selectedIndex > 0">
<div style="position: absolute; margin-left: 8px; margin-top: 5px; scale: 0.8">
<mat-form-field>
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>

View File

@@ -71,4 +71,42 @@
.audio-video-icon {
position: relative;
top: 6px;
}
.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);
}
.media-box:last-child {
border: none;
}
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.remove-item-button {
right: 10px;
position: absolute;
top: 4px;
}
.playlist-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
margin: 0 auto;
}

View File

@@ -5,6 +5,7 @@ import { DatabaseFile, FileType, FileTypeFilter } from '../../../api-types';
import { MatPaginator } from '@angular/material/paginator';
import { Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
@Component({
selector: 'app-recent-videos',
@@ -14,11 +15,25 @@ import { distinctUntilChanged } from 'rxjs/operators';
export class RecentVideosComponent implements OnInit {
@Input() usePaginator = true;
// File selection
@Input() selectMode = false;
@Input() defaultSelected: DatabaseFile[] = [];
@Input() sub_id = null;
@Input() customHeader = null;
@Input() selectedIndex = 1;
@Output() fileSelectionEmitter = new EventEmitter<{new_selection: string[], thumbnailURL: string}>();
pageSize = 10;
paged_data: DatabaseFile[] = null;
selected_data: string[] = [];
selected_data_objs: DatabaseFile[] = [];
reverse_order = false;
// File listing (with cards)
cached_file_count = 0;
loading_files = null;
@@ -63,20 +78,32 @@ export class RecentVideosComponent implements OnInit {
playlists = null;
pageSize = 10;
paged_data: DatabaseFile[] = null;
selected_data: string[] = [];
@ViewChild('paginator') paginator: MatPaginator
constructor(public postsService: PostsService, private router: Router) {
// get cached file count
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
this.loading_files = Array(this.cached_file_count).fill(0);
}
// set filter property to cached value
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
// set file type filter to cached value
const cached_file_type_filter = localStorage.getItem('file_type_filter');
if (this.usePaginator && cached_file_type_filter) {
this.fileTypeFilter = cached_file_type_filter;
}
const sort_order = localStorage.getItem('recent_videos_sort_order');
if (sort_order) {
this.descendingMode = sort_order === 'descending';
}
}
ngOnInit(): void {
@@ -104,23 +131,9 @@ export class RecentVideosComponent implements OnInit {
}
});
// set filter property to cached value
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
// set file type filter to cached value
const cached_file_type_filter = localStorage.getItem('file_type_filter');
if (this.usePaginator && cached_file_type_filter) {
this.fileTypeFilter = cached_file_type_filter;
}
const sort_order = localStorage.getItem('recent_videos_sort_order');
if (sort_order) {
this.descendingMode = sort_order === 'descending';
}
this.selected_data = this.defaultSelected.map(file => file.uid);
this.selected_data_objs = this.defaultSelected;
this.searchChangedSubject
.debounceTime(500)
@@ -364,20 +377,41 @@ export class RecentVideosComponent implements OnInit {
this.getAllFiles();
}
fileSelectionChanged(event): void {
fileSelectionChanged(event: { option: { _selected: boolean; value: DatabaseFile; } }): void {
const adding = event.option._selected;
const value = event.option.value;
if (adding)
this.selected_data.push(value);
else
this.selected_data = this.selected_data.filter(e => e !== value);
let thumbnail_url = null;
if (this.selected_data.length) {
const file_obj = this.paged_data.find(file => file.uid === this.selected_data[0]);
if (file_obj) { thumbnail_url = file_obj['thumbnailURL'] }
if (adding) {
this.selected_data.push(value.uid);
this.selected_data_objs.push(value);
} else {
this.selected_data = this.selected_data.filter(e => e !== value.uid);
this.selected_data_objs = this.selected_data_objs.filter(e => e.uid !== value.uid);
}
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: thumbnail_url});
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
}
toggleSelectionOrder(): void {
this.reverse_order = !this.reverse_order;
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
}
drop(event: CdkDragDrop<string[]>): void {
if (this.reverse_order) {
event.previousIndex = this.selected_data.length - 1 - event.previousIndex;
event.currentIndex = this.selected_data.length - 1 - event.currentIndex;
}
moveItemInArray(this.selected_data, event.previousIndex, event.currentIndex);
moveItemInArray(this.selected_data_objs, event.previousIndex, event.currentIndex);
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
}
removeSelectedFile(index: number): void {
if (this.reverse_order) {
index = this.selected_data.length - 1 - index;
}
this.selected_data.splice(index, 1);
this.selected_data_objs.splice(index, 1);
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
}
}

View File

@@ -1,14 +1,29 @@
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
<form>
<div>
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<app-recent-videos [selectMode]="true" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)"></app-recent-videos>
</div>
</form>
<div class="fixActionRow">
<h4 mat-dialog-title *ngIf="create_mode" ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
<h4 mat-dialog-title *ngIf="!create_mode"><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button>
<mat-dialog-content style="max-height: 85vh;">
<form>
<div *ngIf="create_mode || playlist">
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
</div>
</form>
</mat-dialog-content>
<div class="spacer"></div>
<mat-dialog-actions>
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
<button *ngIf="create_mode" (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>
<ng-container i18n="Create button">Create</ng-container>
</button>
<button *ngIf="!create_mode" (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-flat-button>
<ng-container i18n="Save button">Save</ng-container>
</button>
</mat-dialog-actions>
</div>

View File

@@ -0,0 +1,9 @@
.fixActionRow {
height: 89vh;
display: flex;
flex-direction: column;
}
.spacer {
flex-grow: 1;
}

View File

@@ -2,6 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl } from '@angular/forms';
import { PostsService } from 'app/posts.services';
import { Playlist } from 'api-types';
@Component({
selector: 'app-create-playlist',
@@ -20,9 +21,24 @@ export class CreatePlaylistComponent implements OnInit {
cached_thumbnail_url = null;
create_in_progress = false;
create_mode = false;
constructor(private postsService: PostsService,
public dialogRef: MatDialogRef<CreatePlaylistComponent>) { }
// playlist modify mode
playlist: Playlist = null;
playlist_id: string = null;
preselected_files = [];
playlist_updated = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
private postsService: PostsService,
public dialogRef: MatDialogRef<CreatePlaylistComponent>) {
if (this.data?.create_mode) this.create_mode = true;
if (this.data?.playlist_id) {
this.playlist_id = this.data.playlist_id;
this.getPlaylist();
}
}
ngOnInit(): void {}
@@ -40,6 +56,17 @@ export class CreatePlaylistComponent implements OnInit {
});
}
updatePlaylist(): void {
this.playlist['name'] = this.name;
this.playlist['uids'] = this.filesSelect.value;
this.playlist_updated = true;
this.postsService.updatePlaylist(this.playlist).subscribe(() => {
this.postsService.openSnackBar('Playlist updated successfully.');
this.getPlaylist();
this.postsService.playlists_changed.next(true);
});
}
getThumbnailURL(): string {
return this.cached_thumbnail_url;
}
@@ -49,4 +76,19 @@ export class CreatePlaylistComponent implements OnInit {
if (new_selection.length) this.cached_thumbnail_url = thumbnailURL;
else this.cached_thumbnail_url = null;
}
playlistChanged(): boolean {
return JSON.stringify(this.playlist.uids) !== JSON.stringify(this.filesSelect.value) || this.name !== this.playlist.name;
}
getPlaylist(): void {
this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => {
if (res['playlist']) {
this.filesSelect.setValue(res['file_objs'].map(file => file.uid));
this.preselected_files = res['file_objs'];
this.playlist = res['playlist'];
this.name = this.playlist['name'];
}
});
}
}