From 665bcc04a7f8f9785c9127b44eb09842a992d0c7 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 28 Dec 2022 21:48:24 -0600 Subject: [PATCH] Added ability to favorite a file Moved file filter options above the list of files, and added option to filter for favorites --- Public API v1.yaml | 6 + backend/app.js | 5 + backend/utils.js | 1 + src/api-types/models/DatabaseFile.ts | 1 + src/api-types/models/GetAllFilesRequest.ts | 4 + .../recent-videos.component.html | 35 +++--- .../recent-videos.component.scss | 8 ++ .../recent-videos/recent-videos.component.ts | 106 ++++++++++++++---- .../unified-file-card.component.html | 5 + .../unified-file-card.component.scss | 7 +- .../unified-file-card.component.ts | 6 + .../video-info-dialog.component.html | 5 +- .../video-info-dialog.component.scss | 6 + .../video-info-dialog.component.ts | 14 +++ src/app/posts.services.ts | 4 +- 15 files changed, 173 insertions(+), 40 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index 9b46e55..2752977 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -1746,6 +1746,9 @@ components: description: Filter files by title file_type_filter: $ref: '#/components/schemas/FileTypeFilter' + favorite_filter: + type: boolean + description: If set to true, only gets favorites sub_id: type: string description: Include if you want to filter by subscription @@ -2383,6 +2386,7 @@ components: - upload_date - uploader - url + - favorite type: object properties: id: @@ -2430,6 +2434,8 @@ components: abr: type: number description: In Kbps + favorite: + type: boolean Playlist: required: - uids diff --git a/backend/app.js b/backend/app.js index d2123c4..bdcc7d6 100644 --- a/backend/app.js +++ b/backend/app.js @@ -926,6 +926,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) { const range = req.body.range; const text_search = req.body.text_search; const file_type_filter = req.body.file_type_filter; + const favorite_filter = req.body.favorite_filter; const sub_id = req.body.sub_id; const uuid = req.isAuthenticated() ? req.user.uid : null; @@ -939,6 +940,10 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) { } } + if (favorite_filter) { + filter_obj['favorite'] = true; + } + if (sub_id) { filter_obj['sub_id'] = sub_id; } diff --git a/backend/utils.js b/backend/utils.js index 78d02f1..4ba96eb 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -554,6 +554,7 @@ function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, p this.view_count = view_count; this.height = height; this.abr = abr; + this.favorite = false; } module.exports = { diff --git a/src/api-types/models/DatabaseFile.ts b/src/api-types/models/DatabaseFile.ts index f36af9e..4dfd2b3 100644 --- a/src/api-types/models/DatabaseFile.ts +++ b/src/api-types/models/DatabaseFile.ts @@ -40,4 +40,5 @@ export type DatabaseFile = { * In Kbps */ abr?: number; + favorite: boolean; }; \ No newline at end of file diff --git a/src/api-types/models/GetAllFilesRequest.ts b/src/api-types/models/GetAllFilesRequest.ts index 0acd1c3..cd07f36 100644 --- a/src/api-types/models/GetAllFilesRequest.ts +++ b/src/api-types/models/GetAllFilesRequest.ts @@ -13,6 +13,10 @@ export type GetAllFilesRequest = { */ text_search?: string; file_type_filter?: FileTypeFilter; + /** + * If set to true, only gets favorites + */ + favorite_filter?: boolean; /** * Include if you want to filter by subscription */ diff --git a/src/app/components/recent-videos/recent-videos.component.html b/src/app/components/recent-videos/recent-videos.component.html index 4845f7b..dd3ca3c 100644 --- a/src/app/components/recent-videos/recent-videos.component.html +++ b/src/app/components/recent-videos/recent-videos.component.html @@ -1,12 +1,13 @@
+
- - - {{filterOption['value']['label']}} + + + {{sortOption['value']['label']}} @@ -16,10 +17,12 @@
+

My files

{{customHeader}}

+
Search @@ -28,21 +31,30 @@
+ +
+ + {{filter.value.label}} + +
+
+
- +
No files found.
- -
+ + +
@@ -50,6 +62,7 @@
+
@@ -97,16 +110,6 @@
-
- - File type - - Both - Video only - Audio only - - -
diff --git a/src/app/components/recent-videos/recent-videos.component.scss b/src/app/components/recent-videos/recent-videos.component.scss index bacb397..02e802c 100644 --- a/src/app/components/recent-videos/recent-videos.component.scss +++ b/src/app/components/recent-videos/recent-videos.component.scss @@ -118,4 +118,12 @@ .downloading-spinner { align-self: center; position: absolute; +} + +.filter-list { + margin-bottom: 10px; +} + +.hide { + display: none !important; } \ No newline at end of file diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 90522ff..8ee4be6 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -6,6 +6,8 @@ import { MatPaginator } from '@angular/material/paginator'; import { Subject } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { MatChipListboxChange, MatChipOption } from '@angular/material/chips'; +import { KeyValue } from '@angular/common'; @Component({ selector: 'app-recent-videos', @@ -46,35 +48,54 @@ export class RecentVideosComponent implements OnInit { search_text = ''; searchIsFocused = false; descendingMode = true; - filterProperties = { + sortProperties = { 'registered': { 'key': 'registered', - 'label': 'Download Date', + 'label': $localize`Download Date`, 'property': 'registered' }, 'upload_date': { 'key': 'upload_date', - 'label': 'Upload Date', + 'label': $localize`Upload Date`, 'property': 'upload_date' }, 'name': { 'key': 'name', - 'label': 'Name', + 'label': $localize`Name`, 'property': 'title' }, 'file_size': { 'key': 'file_size', - 'label': 'File Size', + 'label': $localize`File Size`, 'property': 'size' }, 'duration': { 'key': 'duration', - 'label': 'Duration', + 'label': $localize`Duration`, 'property': 'duration' } }; - filterProperty = this.filterProperties['upload_date']; - fileTypeFilter = 'both'; + + fileFilters = { + video_only: { + key: 'video_only', + label: $localize`Video only`, + incompatible: ['audio_only'] + }, + audio_only: { + key: 'audio_only', + label: $localize`Audio only`, + incompatible: ['video_only'] + }, + favorited: { + key: 'favorited', + label: $localize`Favorited` + }, + }; + + selectedFilters = []; + + sortProperty = this.sortProperties['upload_date']; playlists = null; @@ -88,15 +109,17 @@ 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]; + const cached_sort_property = localStorage.getItem('sort_property'); + if (cached_sort_property && this.sortProperties[cached_sort_property]) { + this.sortProperty = this.sortProperties[cached_sort_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 cached_file_filter = localStorage.getItem('file_filter'); + if (this.usePaginator && cached_file_filter) { + this.selectedFilters = JSON.parse(cached_file_filter) + } else { + this.selectedFilters = []; } const sort_order = localStorage.getItem('recent_videos_sort_order'); @@ -107,6 +130,12 @@ export class RecentVideosComponent implements OnInit { } ngOnInit(): void { + if (this.sub_id) { + // subscriptions can't download both audio and video (for now), so don't let users filter for these + delete this.fileFilters['audio_only']; + delete this.fileFilters['video_only']; + } + if (this.postsService.initialized) { this.getAllFiles(); this.getAllPlaylists(); @@ -166,9 +195,37 @@ export class RecentVideosComponent implements OnInit { this.getAllFiles(); } - fileTypeFilterChanged(value: string): void { - localStorage.setItem('file_type_filter', value); - this.getAllFiles(); + filterChanged(value: string): void { + localStorage.setItem('file_filter', value); + // wait a bit for the animation to finish + setTimeout(() => this.getAllFiles(), 150); + } + + selectedFiltersChanged(event: MatChipListboxChange): void { + // in some cases this function will fire even if the selected filters haven't changed + if (event.value.length === this.selectedFilters.length) return; + if (event.value.length > this.selectedFilters.length) { + const filter_key = event.value.filter(possible_new_key => !this.selectedFilters.includes(possible_new_key))[0]; + this.selectedFilters = this.selectedFilters.filter(existing_filter => !this.fileFilters[existing_filter].incompatible || !this.fileFilters[existing_filter].incompatible.includes(filter_key)); + this.selectedFilters.push(filter_key); + } else { + this.selectedFilters = event.value; + } + this.filterChanged(JSON.stringify(this.selectedFilters)); + } + + getFileTypeFilter(): string { + if (this.selectedFilters.includes('audio_only')) { + return 'audio_only'; + } else if (this.selectedFilters.includes('video_only')) { + return 'video_only'; + } else { + return 'both'; + } + } + + getFavoriteFilter(): boolean { + return this.selectedFilters.includes('favorited'); } toggleModeChange(): void { @@ -182,9 +239,11 @@ export class RecentVideosComponent implements OnInit { getAllFiles(cache_mode = false): void { this.normal_files_received = cache_mode; const current_file_index = (this.paginator?.pageIndex ? this.paginator.pageIndex : 0)*this.pageSize; - const sort = {by: this.filterProperty['property'], order: this.descendingMode ? -1 : 1}; + const sort = {by: this.sortProperty['property'], order: this.descendingMode ? -1 : 1}; const range = [current_file_index, current_file_index + this.pageSize]; - this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter as FileTypeFilter, this.sub_id).subscribe(res => { + const fileTypeFilter = this.getFileTypeFilter(); + const favoriteFilter = this.getFavoriteFilter(); + this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, fileTypeFilter as FileTypeFilter, favoriteFilter, this.sub_id).subscribe(res => { this.file_count = res['file_count']; this.paged_data = res['files']; for (let i = 0; i < this.paged_data.length; i++) { @@ -385,4 +444,13 @@ export class RecentVideosComponent implements OnInit { this.selected_data_objs.splice(index, 1); this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL}); } + + originalOrder = (): number => { + return 0; + } + + toggleFavorite(file_obj): void { + file_obj.favorite = !file_obj.favorite; + this.postsService.updateFile(file_obj.uid, {favorite: file_obj.favorite}).subscribe(res => {}); + } } diff --git a/src/app/components/unified-file-card/unified-file-card.component.html b/src/app/components/unified-file-card/unified-file-card.component.html index 6290adf..d8f43c3 100644 --- a/src/app/components/unified-file-card/unified-file-card.component.html +++ b/src/app/components/unified-file-card/unified-file-card.component.html @@ -21,6 +21,11 @@ + diff --git a/src/app/components/unified-file-card/unified-file-card.component.scss b/src/app/components/unified-file-card/unified-file-card.component.scss index ed9084f..c80edc7 100644 --- a/src/app/components/unified-file-card/unified-file-card.component.scss +++ b/src/app/components/unified-file-card/unified-file-card.component.scss @@ -21,12 +21,15 @@ .menuButton { right: 0px; - width: 40px !important; - height: 40px !important; + width: 32px !important; + height: 32px !important; position: absolute; display: flex; align-items: center; z-index: 999; + justify-content: center; + padding: 0px !important; + top: 2px; } /* Coerce the icon container away from display:inline */ diff --git a/src/app/components/unified-file-card/unified-file-card.component.ts b/src/app/components/unified-file-card/unified-file-card.component.ts index 6c4f658..9895abb 100644 --- a/src/app/components/unified-file-card/unified-file-card.component.ts +++ b/src/app/components/unified-file-card/unified-file-card.component.ts @@ -9,6 +9,7 @@ import localeES from '@angular/common/locales/es'; import localeDE from '@angular/common/locales/de'; import localeZH from '@angular/common/locales/zh'; import localeNB from '@angular/common/locales/nb'; +import { DatabaseFile } from 'api-types'; registerLocaleData(localeGB); registerLocaleData(localeFR); @@ -50,6 +51,7 @@ export class UnifiedFileCardComponent implements OnInit { @Input() jwtString = null; @Input() availablePlaylists = null; @Output() goToFile = new EventEmitter(); + @Output() toggleFavorite = new EventEmitter(); @Output() goToSubscription = new EventEmitter(); @Output() deleteFile = new EventEmitter(); @Output() addFileToPlaylist = new EventEmitter(); @@ -158,6 +160,10 @@ export class UnifiedFileCardComponent implements OnInit { this.hide_image = false; } + emitToggleFavorite() { + this.toggleFavorite.emit(this.file_obj); + } + } function fancyTimeFormat(time) { diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.html b/src/app/dialogs/video-info-dialog/video-info-dialog.component.html index 495efbb..ddef003 100644 --- a/src/app/dialogs/video-info-dialog/video-info-dialog.component.html +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.html @@ -1,4 +1,7 @@ -

{{file.title}}

+

+ {{file.title}} + +

diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss b/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss index 96d2ace..a4225c8 100644 --- a/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss @@ -23,4 +23,10 @@ .a-wrap { word-wrap: break-word +} + +.favorite-button { + position: absolute; + right: 4px; + top: 4px; } \ No newline at end of file diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts b/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts index 4d9a7b6..e6c3152 100644 --- a/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts @@ -19,6 +19,7 @@ export class VideoInfoDialogComponent implements OnInit { category: Category; editing = false; initialized = false; + retrieving_file = false; constructor(@Inject(MAT_DIALOG_DATA) public data: any, public postsService: PostsService, private datePipe: DatePipe) { } @@ -58,9 +59,14 @@ export class VideoInfoDialogComponent implements OnInit { } getFile(): void { + this.retrieving_file = true; this.postsService.getFile(this.file.uid).subscribe(res => { + this.retrieving_file = false; this.file = res['file']; this.initializeFile(this.file); + }, err => { + this.retrieving_file = false; + console.error(err); }); } @@ -85,4 +91,12 @@ export class VideoInfoDialogComponent implements OnInit { return JSON.stringify(this.file) !== JSON.stringify(this.new_file); } + toggleFavorite(): void { + this.file.favorite = !this.file.favorite; + this.retrieving_file = true; + this.postsService.updateFile(this.file.uid, {favorite: this.file.favorite}).subscribe(res => { + this.getFile(); + }); + } + } diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 687a548..1cc4ef6 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -377,8 +377,8 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'getFile', body, this.httpOptions); } - getAllFiles(sort: Sort = null, range: number[] = null, text_search: string = null, file_type_filter: FileTypeFilter = FileTypeFilter.BOTH, sub_id: string = null) { - const body: GetAllFilesRequest = {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter, sub_id: sub_id}; + getAllFiles(sort: Sort = null, range: number[] = null, text_search: string = null, file_type_filter: FileTypeFilter = FileTypeFilter.BOTH, favorite_filter = false, sub_id: string = null) { + const body: GetAllFilesRequest = {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter, favorite_filter: favorite_filter, sub_id: sub_id}; return this.http.post(this.path + 'getAllFiles', body, this.httpOptions); }