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.
-
0">
-
+
@@ -97,16 +110,6 @@
0">
-
-
- File type
-
- Both
- Video only
- Audio only
-
-
-
100 ? this.paged_data.length : 250]">
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);
}