This commit is contained in:
Isaac Abadi
2020-08-10 14:27:46 -04:00
47 changed files with 1152 additions and 76 deletions

View File

@@ -38,12 +38,16 @@
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav #sidenav>
<mat-sidenav [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
<mat-divider></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="sidenav.close()" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
</ng-container>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">

View File

@@ -86,6 +86,13 @@ export class AppComponent implements OnInit {
if (!localStorage.getItem('theme')) {
this.setTheme(themingExists ? this.defaultTheme : 'default');
}
// gets the subscriptions
if (this.allowSubscriptions) {
this.postsService.getAllSubscriptions().subscribe(res => {
this.postsService.subscriptions = res['subscriptions'];
})
}
}
// theme stuff
@@ -162,6 +169,11 @@ onSetTheme(theme, old_theme) {
}
getSubscriptions() {
}
goBack() {
if (!this.navigator) {
this.router.navigate(['/home']);

View File

@@ -53,6 +53,7 @@ import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dia
import { SettingsComponent } from './settings/settings.component';
import { MatChipsModule } from '@angular/material/chips';
import { NgxFileDropModule } from 'ngx-file-drop';
import { AvatarModule } from 'ngx-avatar';
import es from '@angular/common/locales/es';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
@@ -73,7 +74,10 @@ import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialo
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
registerLocaleData(es, 'es');
@@ -115,7 +119,10 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
LogsViewerComponent,
ModifyPlaylistComponent,
ConfirmDialogComponent,
EditSubscriptionDialogComponent
UnifiedFileCardComponent,
RecentVideosComponent,
EditSubscriptionDialogComponent,
CustomPlaylistsComponent
],
imports: [
CommonModule,
@@ -156,6 +163,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
DragDropModule,
ClipboardModule,
NgxFileDropModule,
AvatarModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,

View File

@@ -0,0 +1,13 @@
<div *ngIf="playlists && playlists.length > 0">
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)"></app-unified-file-card>
</div>
</div>
</div>
</div>
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
No playlists available. Create one from your downloading files by clicking the blue plus button.
</div>
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>

View File

@@ -0,0 +1,10 @@
.add-playlist-button {
float: right;
position: relative;
bottom: 15px;
right: 15px;
}
.file-col {
max-width: 240px;
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomPlaylistsComponent } from './custom-playlists.component';
describe('CustomPlaylistsComponent', () => {
let component: CustomPlaylistsComponent;
let fixture: ComponentFixture<CustomPlaylistsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CustomPlaylistsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CustomPlaylistsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,112 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
@Component({
selector: 'app-custom-playlists',
templateUrl: './custom-playlists.component.html',
styleUrls: ['./custom-playlists.component.scss']
})
export class CustomPlaylistsComponent implements OnInit {
playlists = null;
playlists_received = false;
downloading_content = {'video': {}, 'audio': {}};
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog) { }
ngOnInit(): void {
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllPlaylists();
}
});
}
getAllPlaylists() {
this.playlists_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.playlists = res['playlists'];
this.playlists_received = true;
});
}
// creating a playlist
openCreatePlaylistDialog() {
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: {
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.getAllPlaylists();
this.postsService.openSnackBar('Successfully created playlist!', '');
} else if (result === false) {
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
}
});
}
goToPlaylist(playlist) {
const playlistID = playlist.id;
const type = playlist.type;
if (playlist) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
}
} else {
// playlist not found
console.error(`Playlist with ID ${playlistID} not found!`);
}
}
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
if (playlistID) { this.downloading_content[type][playlistID] = false };
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
});
}
deletePlaylist(args) {
const playlist = args.file;
const index = args.index;
const playlistID = playlist.id;
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
if (res['success']) {
this.playlists.splice(index, 1);
this.postsService.openSnackBar('Playlist successfully removed.', '');
}
this.getAllPlaylists();
});
}
editPlaylistDialog(args) {
const playlist = args.playlist;
const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: playlist,
width: '65vw'
}
});
dialogRef.afterClosed().subscribe(res => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlists[index] = dialogRef.componentInstance.original_playlist;
}
});
}
}

View File

@@ -0,0 +1,34 @@
<div class="flex-grid">
<div class="col">
<div style="display: inline-block;">
<mat-form-field style="width: 132px;">
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div style="display: inline-block;">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>
<div class="col">
<h4 style="text-align: center">My videos</h4>
</div>
<div class="col" style="top: 25px;">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
<div>
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let file of filtered_files; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
.file-col {
max-width: 240px;
}
.search-bar {
transition: all .5s ease;
position: relative;
float: right;
}
.search-bar-unfocused {
width: 100px;
}
.search-input {
transition: all .5s ease;
}
.search-bar-focused {
width: 200px;
}
.flex-grid {
width: 100%;
display: block;
position: relative;
padding-left: 12px;
padding-right: 12px;
}
.col {
width: 33%;
display: inline-block;
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RecentVideosComponent } from './recent-videos.component';
describe('RecentVideosComponent', () => {
let component: RecentVideosComponent;
let fixture: ComponentFixture<RecentVideosComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RecentVideosComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RecentVideosComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,262 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
@Component({
selector: 'app-recent-videos',
templateUrl: './recent-videos.component.html',
styleUrls: ['./recent-videos.component.scss']
})
export class RecentVideosComponent implements OnInit {
normal_files_received = false;
subscription_files_received = false;
files: any[] = null;
filtered_files: any[] = null;
downloading_content = {'video': {}, 'audio': {}};
search_mode = false;
search_text = '';
searchIsFocused = false;
descendingMode = true;
filterProperties = {
'registered': {
'key': 'registered',
'label': 'Download Date',
'property': 'registered'
},
'upload_date': {
'key': 'upload_date',
'label': 'Upload Date',
'property': 'upload_date'
},
'name': {
'key': 'name',
'label': 'Name',
'property': 'title'
},
'file_size': {
'key': 'file_size',
'label': 'File Size',
'property': 'size'
},
'duration': {
'key': 'duration',
'label': 'Duration',
'property': 'duration'
}
};
filterProperty = this.filterProperties['upload_date'];
constructor(public postsService: PostsService, private router: Router) { }
ngOnInit(): void {
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllFiles();
}
});
// set filter property to cached
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
}
// search
onSearchInputChanged(newvalue) {
if (newvalue.length > 0) {
this.search_mode = true;
this.filterFiles(newvalue);
} else {
this.search_mode = false;
this.filtered_files = this.files;
}
}
private filterFiles(value: string) {
const filterValue = value.toLowerCase();
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
}
filterByProperty(prop) {
if (this.descendingMode) {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
} else {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
}
}
filterOptionChanged(value) {
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
}
toggleModeChange() {
this.descendingMode = !this.descendingMode;
this.filterByProperty(this.filterProperty['property']);
}
// get files
getAllFiles() {
this.normal_files_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.files = res['files'];
this.files.forEach(file => {
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
});
this.files.sort(this.sortFiles);
if (this.search_mode) {
this.filterFiles(this.search_text);
} else {
this.filtered_files = this.files;
}
this.filterByProperty(this.filterProperty['property']);
});
}
// navigation
goToFile(file) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file);
} else {
this.navigateToFile(file);
}
}
navigateToFile(file) {
localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id)
if (sub.streamingOnly) {
this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}]);
} else {
this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
}
} else {
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]);
}
}
goToSubscription(file) {
this.router.navigate(['/subscription', {id: file.sub_id}]);
}
// downloading
downloadFile(file) {
if (file.sub_id) {
this.downloadSubscriptionFile(file);
} else {
this.downloadNormalFile(file);
}
}
downloadSubscriptionFile(file) {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
console.log(sub.isPlaylist)
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
const blob: Blob = res;
saveAs(blob, file.id + ext);
}, err => {
console.log(err);
});
}
downloadNormalFile(file) {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const name = file.id;
this.downloading_content[type][name] = true;
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
this.downloading_content[type][name] = false;
const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + ext);
if (!this.postsService.config.Extra.file_manager_enabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, false).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
}
});
}
// deleting
deleteFile(args) {
const file = args.file;
const index = args.index;
const blacklistMode = args.blacklistMode;
if (file.sub_id) {
this.deleteSubscriptionFile(file, index, blacklistMode);
} else {
this.deleteNormalFile(file, index, blacklistMode);
}
}
deleteNormalFile(file, index, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.files.splice(index, 1);
} else {
this.postsService.openSnackBar('Delete failed!', 'OK.');
}
}, err => {
this.postsService.openSnackBar('Delete failed!', 'OK.');
});
}
deleteSubscriptionFile(file, index, blacklistMode = false) {
if (blacklistMode) {
this.deleteForever(file, index);
} else {
this.deleteAndRedownload(file, index);
}
}
deleteAndRedownload(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.files.splice(index, 1);
});
}
deleteForever(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.files.splice(index, 1);
});
}
// sorting and filtering
sortFiles(a, b) {
// uses the 'registered' flag as the timestamp
const result = b.registered - a.registered;
return result;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

View File

@@ -0,0 +1,39 @@
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
<div class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>&nbsp;&nbsp;{{file_obj.registered | date:'shortDate'}}</div>
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<ng-container *ngIf="!is_playlist">
<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>
<mat-divider></mat-divider>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
</button>
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container>
</button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</ng-container>
<ng-container *ngIf="is_playlist">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<mat-divider></mat-divider>
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</ng-container>
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile()" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium'}" [src]="file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time">
{{file_length}}
</div>
</div>
</div>
<span [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }"><strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
</div>
</mat-card>
</div>

View File

@@ -0,0 +1,117 @@
.file-mat-card {
width: 200px;
height: 200px;
padding: 0px;
cursor: pointer;
}
.small-mat-card {
width: 150px;
height: 150px;
padding: 0px;
cursor: pointer;
}
.menuButton {
right: 0px;
top: -1px;
position: absolute;
z-index: 999;
}
/* Coerce the <span> icon container away from display:inline */
.mat-icon-button .mat-button-wrapper {
display: flex;
justify-content: center;
}
.image {
width: 200px;
height: 112.5px;
object-fit: cover;
}
.image-small {
width: 150px;
height: 84.5px;
object-fit: cover;
}
.example-full-width-height {
width: 100%;
height: 100%
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.img-div {
max-height: 80px;
padding: 0px;
margin: 32px 0px 0px -5px;
width: calc(100% + 5px + 5px);
}
.max-two-lines {
display: -webkit-box;
display: -moz-box;
max-height: 2.4em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.max-one-line {
display: -webkit-box;
display: -moz-box;
max-height: 1.2em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.duration-time {
position: absolute;
bottom: 5px;
right: 5px;
z-index: 99999;
background: rgba(255,255,255,0.6);
padding-left: 5px;
padding-right: 5px;
}
.download-time {
position: absolute;
top: 1px;
left: 5px;
z-index: 99999;
}
.audio-video-icon {
position: relative;
top: 6px;
}
@media (max-width: 576px){
// .example-card {
// width: 175px !important;
// }
// .image {
// width: 175px;
// }
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UnifiedFileCardComponent } from './unified-file-card.component';
describe('UnifiedFileCardComponent', () => {
let component: UnifiedFileCardComponent;
let fixture: ComponentFixture<UnifiedFileCardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UnifiedFileCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UnifiedFileCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,95 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
@Component({
selector: 'app-unified-file-card',
templateUrl: './unified-file-card.component.html',
styleUrls: ['./unified-file-card.component.scss']
})
export class UnifiedFileCardComponent implements OnInit {
// required info
file_title = '';
file_length = '';
file_thumbnail = '';
type = null;
elevated = false;
@Input() file_obj = null;
@Input() card_size = 'medium';
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Output() goToFile = new EventEmitter<any>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() editPlaylist = new EventEmitter<any>();
/*
Planned sizes:
small: 150x175
medium: 200x200
big: 250x200
*/
constructor(private dialog: MatDialog) { }
ngOnInit(): void {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
emitDeleteFile(blacklistMode = false) {
this.deleteFile.emit({
file: this.file_obj,
index: this.index,
blacklistMode: blacklistMode
});
}
navigateToFile() {
this.goToFile.emit(this.file_obj);
}
navigateToSubscription() {
this.goToSubscription.emit(this.file_obj);
}
openFileInfoDialog() {
this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.file_obj,
},
minWidth: '50vw'
})
}
emitEditPlaylist() {
this.editPlaylist.emit({
playlist: this.file_obj,
index: this.index
});
}
}
function fancyTimeFormat(time) {
if (typeof time === 'string') {
return time;
}
// Hours, minutes and seconds
const hrs = ~~(time / 3600);
const mins = ~~((time % 3600) / 60);
const secs = ~~time % 60;
// Output like "1:01" or "4:03:59" or "123:03:59"
let ret = '';
if (hrs > 0) {
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
}
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
ret += '' + secs;
return ret;
}

View File

@@ -1,18 +1,34 @@
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
<form>
<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>
<div>
<mat-form-field color="accent">
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="filesToSelectFrom || (audiosToSelectFrom && videosToSelectFrom)">
<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>
<div *ngIf="!filesToSelectFrom">
<mat-form-field color="accent">
<mat-select placeholder="Type" i18n-placeholder="Type select" [(ngModel)]="type" [ngModelOptions]="{standalone: true}">
<mat-option value="audio"><ng-container i18n="Audio">Audio</ng-container></mat-option>
<mat-option value="video"><ng-container i18n="Video">Video</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length > 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length > 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length > 0))" color="accent">
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
</mat-select>
</mat-form-field>
<!-- No videos available -->
<div style="margin-bottom: 15px;" *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length === 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length === 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length === 0))">
No files available.
</div>
</div>
</div>
</form>

View File

@@ -14,6 +14,8 @@ export class CreatePlaylistComponent implements OnInit {
filesToSelectFrom = null;
type = null;
filesSelect = new FormControl();
audiosToSelectFrom = null;
videosToSelectFrom = null;
name = '';
create_in_progress = false;
@@ -28,12 +30,30 @@ export class CreatePlaylistComponent implements OnInit {
this.filesToSelectFrom = this.data.filesToSelectFrom;
this.type = this.data.type;
}
if (!this.filesToSelectFrom) {
this.getMp3s();
this.getMp4s();
}
}
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
this.audiosToSelectFrom = result['mp3s'];
});
}
getMp4s() {
this.postsService.getMp4s().subscribe(result => {
this.videosToSelectFrom = result['mp4s'];
});
}
createPlaylist() {
const thumbnailURL = this.getThumbnailURL();
const duration = this.calculateDuration();
this.create_in_progress = true;
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => {
this.create_in_progress = false;
if (res['success']) {
this.dialogRef.close(true);
@@ -44,8 +64,12 @@ export class CreatePlaylistComponent implements OnInit {
}
getThumbnailURL() {
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
const file = this.filesToSelectFrom[i];
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === this.filesSelect.value[0]) {
// different services store the thumbnail in different places
if (file.thumbnailURL) { return file.thumbnailURL };
@@ -55,4 +79,35 @@ export class CreatePlaylistComponent implements OnInit {
return null;
}
getDuration(file_id) {
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === file_id) {
return file.duration;
}
}
return null;
}
calculateDuration() {
let sum = 0;
for (let i = 0; i < this.filesSelect.value.length; i++) {
const duration_val = this.getDuration(this.filesSelect.value[i]);
sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val;
}
return sum;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

View File

@@ -24,6 +24,34 @@
<p>
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
</p>
<mat-divider></mat-divider>
<div style="margin-top: 10px;">
<h5>Personal settings:</h5>
<mat-form-field placeholder="Sidepanel mode">
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
<mat-option value="over">
Over
</mat-option>
<mat-option value="side">
Side
</mat-option>
</mat-select>
</mat-form-field>
<br/>
<mat-form-field placeholder="Card size">
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
<mat-option value="large" [disabled]="true">
Large
</mat-option>
<mat-option value="medium">
Medium
</mat-option>
<mat-option value="small">
Small
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-dialog-content>

View File

@@ -16,11 +16,13 @@ export class AboutDialogComponent implements OnInit {
checking_for_updates = true;
current_version_tag = CURRENT_VERSION;
sidepanel_mode = this.postsService.sidepanel_mode;
card_size = this.postsService.card_size;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getLatestGithubRelease()
this.getLatestGithubRelease();
}
getLatestGithubRelease() {
@@ -30,4 +32,14 @@ export class AboutDialogComponent implements OnInit {
});
}
sidePanelModeChanged(new_mode) {
localStorage.setItem('sidepanel_mode', new_mode);
this.postsService.sidepanel_mode = new_mode;
}
cardSizeOptionChanged(new_size) {
localStorage.setItem('card_size', new_size);
this.postsService.card_size = new_size;
}
}

View File

@@ -1,10 +1,7 @@
<br/>
<div class="big demo-basic">
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : null">
<mat-card-title>
<ng-container i18n="Youtube downloader home page label">Youtube Downloader</ng-container>
</mat-card-title>
<mat-card-content>
<mat-card-content style="padding: 0px 8px 0px 8px;">
<div style="position: relative;">
<form class="example-form">
<div class="container-fluid">
@@ -183,7 +180,13 @@
<ng-template #nofile>
</ng-template>
<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<mat-accordion>
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
<mat-expansion-panel-header>
@@ -260,7 +263,7 @@
</mat-grid-tile>
</mat-grid-list>
<!-- Add video playlist button -->
<!-- Add video playlist button --<
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
<div *ngIf="playlists.video.length === 0">
<ng-container i18n="No video playlists available text">
@@ -270,12 +273,4 @@
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template #nomp3s>
</ng-template>
<ng-template #nomp4s>
</ng-template>
</div>-->

View File

@@ -65,6 +65,13 @@
padding-right: 0px;
padding-left: 0.01px;
height: 100%;
width: 100%;
}
.audio-col {
margin: 0 auto;
margin-top: 10px;
width: 95%;
}
.save-icon {

View File

@@ -1,13 +1,13 @@
<div *ngIf="playlist.length > 0 && show_player">
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
<div style="max-width: 100%; margin-left: 0px; height: 70vh" class="row">
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
</video>
</vg-player>
</div>
<div class="col-12 my-2">
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>

View File

@@ -22,6 +22,8 @@ export class PostsService implements CanActivate {
handShakeComplete = false;
THEMES_CONFIG = THEMES_CONFIG;
theme;
card_size = 'medium';
sidepanel_mode = 'over';
settings_changed = new BehaviorSubject<boolean>(false);
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null;
@@ -47,6 +49,7 @@ export class PostsService implements CanActivate {
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
config = null;
subscriptions = null;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar) {
console.log('PostsService Initialized...');
@@ -101,6 +104,14 @@ export class PostsService implements CanActivate {
this.reload_config.subscribe(yes_reload => {
if (yes_reload) { this.reloadConfig(); }
});
if (localStorage.getItem('sidepanel_mode')) {
this.sidepanel_mode = localStorage.getItem('sidepanel_mode');
}
if (localStorage.getItem('card_size')) {
this.card_size = localStorage.getItem('card_size');
}
}
canActivate(route, state): Promise<boolean> {
return new Promise(resolve => {
@@ -114,6 +125,15 @@ export class PostsService implements CanActivate {
this.theme = this.THEMES_CONFIG[theme];
}
getSubscriptionByID(sub_id) {
for (let i = 0; i < this.subscriptions.length; i++) {
if (this.subscriptions[i]['id'] === sub_id) {
return this.subscriptions[i];
}
}
return null;
}
startHandshake(url: string) {
return this.http.get(url + 'geturl');
}
@@ -204,6 +224,10 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'getFile', {uid: uid, type: type, uuid: uuid}, this.httpOptions);
}
getAllFiles() {
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
uid = null, uuid = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
@@ -251,11 +275,12 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
}
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) {
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
fileNames: fileNames,
type: type,
thumbnailURL: thumbnailURL}, this.httpOptions);
thumbnailURL: thumbnailURL,
duration: duration}, this.httpOptions);
}
getPlaylist(playlistID, type, uuid = null) {

View File

@@ -79,11 +79,6 @@
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']"><ng-container i18n="Use youtube-dl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist"><ng-container i18n="youtube-dl archive explanation prefix link">With youtube-dl's archive</ng-container></a>&nbsp;<ng-container i18n="youtube-dl archive explanation middle">feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory.</ng-container></p>
<p><ng-container i18n="youtube-dl archive explanation suffix">This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</ng-container></p>
</div>
</div>
</div>
<mat-divider></mat-divider>
@@ -150,7 +145,6 @@
<div class="col-12 mt-5">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p><ng-container i18n="youtubedl archive setting Note">Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head to the Main tab and activate this option there.</ng-container></p>
</div>
<div class="col-12 mt-2">

View File

@@ -108,4 +108,4 @@ function fancyTimeFormat(time) {
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
ret += '' + secs;
return ret;
}
}

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
@@ -45,9 +45,21 @@ export class SubscriptionComponent implements OnInit {
filterProperty = this.filterProperties['upload_date'];
downloading = false;
initialized = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
ngOnInit() {
this.route.paramMap.subscribe((params: ParamMap) => {
this.id = params.get('id');
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.initialized = true;
this.getConfig();
this.getSubscription();
}
});
});
if (this.route.snapshot.paramMap.get('id')) {
this.id = this.route.snapshot.paramMap.get('id');
@@ -84,7 +96,7 @@ export class SubscriptionComponent implements OnInit {
}
getConfig() {
this.use_youtubedl_archive = this.postsService.config['Subscriptions']['subscriptions_use_youtubedl_archive'];
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
}
goToFile(emit_obj) {
@@ -123,7 +135,6 @@ export class SubscriptionComponent implements OnInit {
}
filterOptionChanged(value) {
// this.filterProperty = value;
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
}

View File

@@ -12,7 +12,7 @@
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"use_youtubedl_archive": true,
"custom_args": "",
"safe_download_override": false
},