Added ability to favorite a file

Moved file filter options above the list of files, and added option to filter for favorites
pull/809/head
Isaac Abadi 2 years ago
parent c45e0f04be
commit 665bcc04a7

@ -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

@ -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;
}

@ -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 = {

@ -40,4 +40,5 @@ export type DatabaseFile = {
* In Kbps
*/
abr?: number;
favorite: boolean;
};

@ -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
*/

@ -1,12 +1,13 @@
<div class="container-fluid" style="max-width: 941px;">
<div class="row">
<!-- Sorting -->
<div class="col-12 order-2 col-sm-4 order-sm-1 d-flex justify-content-center">
<div>
<div style="display: inline-block;">
<mat-form-field appearance="outline" style="width: 165px;">
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
<mat-select [(ngModel)]="this.sortProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let sortOption of sortProperties | keyvalue" [value]="sortOption.value">
{{sortOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
@ -16,10 +17,12 @@
</div>
</div>
</div>
<!-- Files title -->
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
<h4 *ngIf="!customHeader" class="my-videos-title" i18n="My files title">My files</h4>
<h4 *ngIf="customHeader" class="my-videos-title">{{customHeader}}</h4>
</div>
<!-- Search -->
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
<mat-form-field appearance="outline" [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<mat-label i18n="Search">Search</mat-label>
@ -28,21 +31,30 @@
</mat-form-field>
</div>
</div>
<!-- Filters -->
<div class="row justify-content-center">
<mat-chip-listbox class="filter-list" [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
<mat-chip-option *ngFor="let filter of fileFilters | keyvalue: originalOrder" [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
</mat-chip-listbox>
</div>
</div>
<div>
<!-- Files -->
<div *ngIf="!selectMode" class="container" style="margin-bottom: 16px">
<div class="row justify-content-center">
<!-- Real cards -->
<ng-container *ngIf="normal_files_received && paged_data">
<div style="display: flex; align-items: center;" *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" (toggleFavorite)="toggleFavorite($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
<mat-spinner *ngIf="downloading_content[file.uid]" class="downloading-spinner" [diameter]="32"></mat-spinner>
</div>
<div *ngIf="paged_data.length === 0">
<ng-container i18n="No files found">No files found.</ng-container>
</div>
</ng-container>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<!-- Fake cards -->
<ng-container>
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[normal_files_received ? 'hide' : '', postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
</ng-container>
@ -50,6 +62,7 @@
</div>
<div *ngIf="selectMode">
<!-- If selected files e.g. for creating a playlist -->
<mat-tab-group [(selectedIndex)]="selectedIndex">
<mat-tab label="Order" i18n-label="Order">
<div *ngIf="selected_data.length">
@ -97,16 +110,6 @@
</div>
<div style="position: relative;" *ngIf="usePaginator && selectedIndex > 0">
<div style="position: absolute; margin-left: 8px; margin-top: -2px; scale: 0.8">
<mat-form-field>
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="fileTypeFilter" (selectionChange)="fileTypeFilterChanged($event.value)">
<mat-option value="both"><ng-container i18n="Both">Both</ng-container></mat-option>
<mat-option value="video_only"><ng-container i18n="Video only">Video only</ng-container></mat-option>
<mat-option value="audio_only"><ng-container i18n="Audio only">Audio only</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-paginator class="paginator" #paginator (page)="pageChangeEvent($event)" [length]="file_count"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">

@ -118,4 +118,12 @@
.downloading-spinner {
align-self: center;
position: absolute;
}
.filter-list {
margin-bottom: 10px;
}
.hide {
display: none !important;
}

@ -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 => {});
}
}

@ -21,6 +21,11 @@
</mat-menu>
<mat-menu #action_menu="matMenu">
<ng-container *ngIf="!is_playlist && !loading">
<button (click)="emitToggleFavorite()" mat-menu-item>
<mat-icon>{{file_obj.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon>
<ng-container *ngIf="!file_obj.favorite" i18n="Favorite button">Favorite</ng-container>
<ng-container *ngIf="file_obj.favorite" i18n="Unfavorite button">Unfavorite</ng-container>
</button>
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
<button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon>&nbsp;<ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>

@ -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 <span> icon container away from display:inline */

@ -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<any>();
@Output() toggleFavorite = new EventEmitter<DatabaseFile>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() addFileToPlaylist = new EventEmitter<any>();
@ -158,6 +160,10 @@ export class UnifiedFileCardComponent implements OnInit {
this.hide_image = false;
}
emitToggleFavorite() {
this.toggleFavorite.emit(this.file_obj);
}
}
function fancyTimeFormat(time) {

@ -1,4 +1,7 @@
<h4 mat-dialog-title>{{file.title}}</h4>
<h4 mat-dialog-title>
{{file.title}}
<button [disabled]="!initialized || retrieving_file" (click)="toggleFavorite()" mat-icon-button class="favorite-button"><mat-icon>{{file.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon></button>
</h4>
<mat-dialog-content>
<div style="width: 100%; position: relative;">

@ -23,4 +23,10 @@
.a-wrap {
word-wrap: break-word
}
.favorite-button {
position: absolute;
right: 4px;
top: 4px;
}

@ -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();
});
}
}

@ -377,8 +377,8 @@ export class PostsService implements CanActivate {
return this.http.post<GetFileResponse>(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<GetAllFilesResponse>(this.path + 'getAllFiles', body, this.httpOptions);
}

Loading…
Cancel
Save