Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into api-generator

This commit is contained in:
Isaac Abadi
2021-09-29 02:38:42 -06:00
199 changed files with 46018 additions and 15941 deletions

View File

@@ -7,19 +7,21 @@ import { SubscriptionComponent } from './subscription/subscription/subscription.
import { PostsService } from './posts.services';
import { LoginComponent } from './components/login/login.component';
import { DownloadsComponent } from './components/downloads/downloads.component';
import { SettingsComponent } from './settings/settings.component';
const routes: Routes = [
{ path: 'home', component: MainComponent, canActivate: [PostsService] },
{ path: 'player', component: PlayerComponent, canActivate: [PostsService]},
{ path: 'subscriptions', component: SubscriptionsComponent, canActivate: [PostsService] },
{ path: 'subscription', component: SubscriptionComponent, canActivate: [PostsService] },
{ path: 'settings', component: SettingsComponent, canActivate: [PostsService] },
{ path: 'login', component: LoginComponent },
{ path: 'downloads', component: DownloadsComponent },
{ path: 'downloads', component: DownloadsComponent, canActivate: [PostsService] },
{ path: '', redirectTo: '/home', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
imports: [RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' })],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -23,10 +23,10 @@
<span i18n="Dark mode toggle label">Dark</span>
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
</button>
<button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
<!-- <button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
<mat-icon>settings</mat-icon>
<span i18n="Settings menu label">Settings</span>
</button>
</button> -->
<button (click)="openAboutDialog()" mat-menu-item>
<mat-icon>info</mat-icon>
<span i18n="About menu label">About</span>
@@ -42,10 +42,14 @@
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" 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)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" 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)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" 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')))">
<a *ngIf="postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<ng-container *ngIf="postsService.config && postsService.hasPermission('settings')">
<mat-divider></mat-divider>
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
</ng-container>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')">
<mat-divider *ngIf="postsService.subscriptions.length > 0"></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar>{{subscription.name}}</a>
</ng-container>
</mat-nav-list>

View File

@@ -1,9 +1,9 @@
import { TestBed, async } from '@angular/core/testing';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
@@ -11,19 +11,19 @@ describe('AppComponent', () => {
}).compileComponents();
}));
it('should create the app', async(() => {
it('should create the app', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
it(`should have as title 'app'`, waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
it('should render title in a h1 tag', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;

View File

@@ -1,9 +1,6 @@
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
import {MatDialogRef} from '@angular/material/dialog';
import {PostsService} from './posts.services';
import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
import {FormControl, Validators} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { MatDialog } from '@angular/material/dialog';
import { MatSidenav } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -16,7 +13,6 @@ import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/do'
import 'rxjs/add/operator/switch'
import { YoutubeSearchService, Result } from './youtube-search.service';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { OverlayContainer } from '@angular/cdk/overlay';
import { THEMES_CONFIG } from '../themes';
@@ -28,7 +24,11 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.css'],
providers: [{
provide: MatDialogRef,
useValue: {}
}]
})
export class AppComponent implements OnInit, AfterViewInit {
@@ -116,6 +116,12 @@ export class AppComponent implements OnInit, AfterViewInit {
if (this.allowSubscriptions) {
this.postsService.reloadSubscriptions();
}
this.postsService.reloadCategories();
this.postsService.getVersionInfo().subscribe(res => {
this.postsService.version_info = res['version_info'];
});
}
// theme stuff

View File

@@ -30,16 +30,19 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { TextFieldModule } from '@angular/cdk/text-field';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { PostsService } from 'app/posts.services';
import { FileCardComponent } from './file-card/file-card.component';
import { RouterModule } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { MainComponent } from './main/main.component';
import { PlayerComponent } from './player/player.component';
import { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
import { VgControlsModule } from '@videogular/ngx-videogular/controls';
import { VgBufferingModule } from '@videogular/ngx-videogular/buffering';
import { VgOverlayPlayModule } from '@videogular/ngx-videogular/overlay-play';
import { VgCoreModule } from '@videogular/ngx-videogular/core';
import { InputDialogComponent } from './input-dialog/input-dialog.component';
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
@@ -79,6 +82,12 @@ import { UnifiedFileCardComponent } from './components/unified-file-card/unified
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';
import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit-category-dialog.component';
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
import { H401Interceptor } from './http.interceptor';
import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component';
import { SkipAdButtonComponent } from './components/skip-ad-button/skip-ad-button.component';
registerLocaleData(es, 'es');
@@ -89,7 +98,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
@NgModule({
declarations: [
AppComponent,
FileCardComponent,
MainComponent,
PlayerComponent,
InputDialogComponent,
@@ -105,6 +113,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
VideoInfoDialogComponent,
ArgModifierDialogComponent,
HighlightPipe,
LinkifyPipe,
UpdaterComponent,
UpdateProgressDialogComponent,
ShareMediaDialogComponent,
@@ -123,7 +132,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
UnifiedFileCardComponent,
RecentVideosComponent,
EditSubscriptionDialogComponent,
CustomPlaylistsComponent
CustomPlaylistsComponent,
EditCategoryDialogComponent,
TwitchChatComponent,
SeeMoreComponent,
ConcurrentStreamComponent,
SkipAdButtonComponent
],
imports: [
CommonModule,
@@ -162,6 +176,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatChipsModule,
DragDropModule,
ClipboardModule,
TextFieldModule,
NgxFileDropModule,
AvatarModule,
ContentLoaderModule,
@@ -181,10 +196,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
SettingsComponent
],
providers: [
PostsService
PostsService,
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true }
],
exports: [
HighlightPipe
HighlightPipe,
LinkifyPipe
],
bootstrap: [AppComponent]
})

View File

@@ -0,0 +1,6 @@
<div class="buttons-container">
<button (click)="startWatching()" *ngIf="!watch_together_clicked" mat-flat-button>Watch together</button>
<button (click)="startServer()" *ngIf="watch_together_clicked && !started && server_mode && server_already_exists === false" mat-flat-button>Start stream</button>
<button (click)="startClient()" *ngIf="watch_together_clicked && !started && server_already_exists === true" mat-flat-button>Join stream</button>
<button style="margin-left: 10px;" (click)="stop()" *ngIf="watch_together_clicked" mat-flat-button>Stop</button>
</div>

View File

@@ -0,0 +1,7 @@
.buttons-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 15px;
margin-bottom: 15px;
}

View File

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

View File

@@ -0,0 +1,140 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-concurrent-stream',
templateUrl: './concurrent-stream.component.html',
styleUrls: ['./concurrent-stream.component.scss']
})
export class ConcurrentStreamComponent implements OnInit {
@Input() server_mode = false;
@Input() playback_timestamp;
@Input() playing;
@Input() uid;
@Output() setPlaybackTimestamp = new EventEmitter<any>();
@Output() togglePlayback = new EventEmitter<boolean>();
@Output() setPlaybackRate = new EventEmitter<number>();
started = false;
server_started = false;
watch_together_clicked = false;
server_already_exists = null;
check_timeout: any;
update_timeout: any;
PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION = 0.5;
PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP = 2;
PLAYBACK_MODIFIER = 0.1;
playback_rate_modified = false;
constructor(private postsService: PostsService) { }
// flow: click start watching -> check for available stream to enable join button and if user, display "start stream"
// users who join a stream will send continuous requests for info on playback
ngOnInit(): void {
}
startServer() {
this.started = true;
this.server_started = true;
this.update_timeout = setInterval(() => {
this.updateStream();
}, 1000);
}
updateStream() {
this.postsService.updateConcurrentStream(this.uid, this.playback_timestamp, Date.now()/1000, this.playing).subscribe(res => {
});
}
startClient() {
this.started = true;
}
checkStream() {
if (this.server_started) { return; }
const current_playback_timestamp = this.playback_timestamp;
const current_unix_timestamp = Date.now()/1000;
this.postsService.checkConcurrentStream(this.uid).subscribe(res => {
const stream = res['stream'];
if (!stream) {
this.server_already_exists = false;
return;
}
this.server_already_exists = true;
// check whether client has joined the stream
if (!this.started) { return; }
if (!stream['playing'] && this.playing) {
// tell client to pause and set the timestamp to sync
this.togglePlayback.emit(false);
this.setPlaybackTimestamp.emit(stream['playback_timestamp']);
} else if (stream['playing']) {
// sync unpause state
if (!this.playing) { this.togglePlayback.emit(true); }
// sync time
const zeroed_local_unix_timestamp = current_unix_timestamp - current_playback_timestamp;
const zeroed_server_unix_timestamp = stream['unix_timestamp'] - stream['playback_timestamp'];
const seconds_behind_locally = zeroed_local_unix_timestamp - zeroed_server_unix_timestamp;
if (Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP) {
// skip to playback timestamp because the difference is too high
this.setPlaybackTimestamp.emit(this.playback_timestamp + seconds_behind_locally + 0.3);
this.playback_rate_modified = false;
} else if (!this.playback_rate_modified && Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION) {
// increase playback speed to avoid skipping
let seconds_to_wait = (Math.abs(seconds_behind_locally)/this.PLAYBACK_MODIFIER);
seconds_to_wait += 0.3/this.PLAYBACK_MODIFIER;
this.playback_rate_modified = true;
if (seconds_behind_locally > 0) {
// increase speed
this.setPlaybackRate.emit(1 + this.PLAYBACK_MODIFIER);
setTimeout(() => {
this.setPlaybackRate.emit(1);
this.playback_rate_modified = false;
}, seconds_to_wait * 1000);
} else {
// decrease speed
this.setPlaybackRate.emit(1 - this.PLAYBACK_MODIFIER);
setTimeout(() => {
this.setPlaybackRate.emit(1);
this.playback_rate_modified = false;
}, seconds_to_wait * 1000);
}
}
}
});
}
startWatching() {
this.watch_together_clicked = true;
this.check_timeout = setInterval(() => {
this.checkStream();
}, 1000);
}
stop() {
if (this.check_timeout) { clearInterval(this.check_timeout); }
if (this.update_timeout) { clearInterval(this.update_timeout); }
this.started = false;
this.server_started = false;
this.watch_together_clicked = false;
}
}

View File

@@ -2,7 +2,7 @@
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [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 [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CustomPlaylistsComponent } from './custom-playlists.component';
@@ -6,7 +6,7 @@ describe('CustomPlaylistsComponent', () => {
let component: CustomPlaylistsComponent;
let fixture: ComponentFixture<CustomPlaylistsComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CustomPlaylistsComponent ]
})

View File

@@ -24,11 +24,18 @@ export class CustomPlaylistsComponent implements OnInit {
this.getAllPlaylists();
}
});
this.postsService.playlists_changed.subscribe(changed => {
if (changed) {
this.getAllPlaylists();
}
});
}
getAllPlaylists() {
this.playlists_received = false;
this.postsService.getAllFiles().subscribe(res => {
// must call getAllFiles as we need to get category playlists as well
this.postsService.getPlaylists().subscribe(res => {
this.playlists = res['playlists'];
this.playlists_received = true;
});
@@ -53,16 +60,15 @@ export class CustomPlaylistsComponent implements OnInit {
goToPlaylist(info_obj) {
const playlist = info_obj.file;
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);
this.downloadPlaylist(playlist.id, playlist.name);
} 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}]);
const routeParams = {playlist_id: playlistID};
if (playlist.auto) { routeParams['auto'] = playlist.auto; }
this.router.navigate(['/player', routeParams]);
}
} else {
// playlist not found
@@ -70,11 +76,12 @@ export class CustomPlaylistsComponent implements OnInit {
}
}
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
this.postsService.downloadFileFromServer(fileNames, type, {outputName: zipName}).subscribe(res => {
if (playlistID) { this.downloading_content[type][playlistID] = false };
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
downloadPlaylist(playlist_id, playlist_name) {
this.downloading_content[playlist_id] = true;
this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => {
this.downloading_content[playlist_id] = false;
const blob: any = res;
saveAs(blob, playlist_name + '.zip');
});
}
@@ -97,7 +104,7 @@ export class CustomPlaylistsComponent implements OnInit {
const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: playlist,
playlist_id: playlist.id,
width: '65vw'
}
});

View File

@@ -1,27 +1,91 @@
<div style="padding: 20px;">
<div *ngFor="let session_downloads of downloads | keyvalue">
<ng-container *ngIf="keys(session_downloads.value).length > 0">
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container>&nbsp;{{session_downloads.key}}
<span *ngIf="session_downloads.key === postsService.session_id">&nbsp;<ng-container i18n="Current session">(current)</ng-container></span>
</h4>
<div class="container">
<div class="row">
<div *ngFor="let download of session_downloads.value | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
<mat-card *ngIf="download.value" class="mat-elevation-z3">
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads.key, download.value.uid)"></app-download-item>
</mat-card>
</div>
</div>
</div>
<div>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
</div>
</mat-card>
</ng-container>
</div>
<div [hidden]="!(downloads && downloads.length > 0)">
<div style="overflow: hidden;" [ngClass]="uids ? 'rounded mat-elevation-z2' : 'mat-elevation-z8'">
<mat-table style="overflow: hidden" [ngClass]="uids ? 'rounded-top' : null" matSort [dataSource]="dataSource">
<!-- Date Column -->
<ng-container matColumnDef="date">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.timestamp_start | date: 'short'}} </mat-cell>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
</mat-cell>
</ng-container>
<div *ngIf="downloads && !downloadsValid()">
<h4 style="text-align: center;" i18n="No downloads label">No downloads available!</h4>
</div>
<!-- Subscription Column -->
<ng-container matColumnDef="subscription">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Subscription">Subscription</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<ng-container *ngIf="element.sub_name">
{{element.sub_name}}
</ng-container>
<ng-container *ngIf="!element.sub_name">
N/A
</ng-container>
</mat-cell>
</ng-container>
<!-- Stage Column -->
<ng-container matColumnDef="stage">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Stage">Stage</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> {{STEP_INDEX_TO_LABEL[element.step_index]}} </mat-cell>
</ng-container>
<!-- Progress Column -->
<ng-container matColumnDef="progress">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<ng-container *ngIf="element.percent_complete">
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
</ng-container>
<ng-container *ngIf="!element.percent_complete">
N/A
</ng-container>
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<div>
<ng-container *ngIf="!element.finished">
<button (click)="pauseDownload(element.uid)" *ngIf="!element.paused || !element.finished_step" [disabled]="element.paused && !element.finished_step" mat-icon-button matTooltip="Pause" i18n-matTooltip="Pause"><mat-spinner [diameter]="28" *ngIf="element.paused && !element.finished_step" class="icon-button-spinner"></mat-spinner><mat-icon>pause</mat-icon></button>
<button (click)="resumeDownload(element.uid)" *ngIf="element.paused && element.finished_step" mat-icon-button matTooltip="Resume" i18n-matTooltip="Resume"><mat-icon>play_arrow</mat-icon></button>
<button *ngIf="false && !element.paused" (click)="cancelDownload(element.uid)" mat-icon-button matTooltip="Cancel" i18n-matTooltip="Cancel"><mat-icon>cancel</mat-icon></button>
</ng-container>
<ng-container *ngIf="element.finished">
<button *ngIf="!element.error" (click)="watchContent(element)" mat-icon-button matTooltip="Watch content" i18n-matTooltip="Watch content"><mat-icon>smart_display</mat-icon></button>
<button *ngIf="element.error" (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
<button (click)="restartDownload(element.uid)" mat-icon-button matTooltip="Restart" i18n-matTooltip="Restart"><mat-icon>restart_alt</mat-icon></button>
</ng-container>
<button *ngIf="element.finished || element.paused" (click)="clearDownload(element.uid)" mat-icon-button matTooltip="Clear" i18n-matTooltip="Clear"><mat-icon>delete</mat-icon></button>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="uids ? 'rounded-top' : null" *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-paginator [ngClass]="uids ? 'rounded-bottom' : null" [pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
aria-label="Select page of downloads">
</mat-paginator>
</div>
<div *ngIf="!uids" class="downloads-action-button-div">
<button [disabled]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
<button style="margin-left: 10px;" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
<button color="warn" style="margin-left: 10px;" mat-stroked-button (click)="clearFinishedDownloads()"><ng-container i18n="Clear finished downloads">Clear finished downloads</ng-container></button>
</div>
</div>
<div *ngIf="(!downloads || downloads.length === 0) && downloads_retrieved && !uids">
<h4 style="text-align: center; margin-top: 10px;" i18n="No downloads label">No downloads available!</h4>
</div>

View File

@@ -0,0 +1,32 @@
mat-header-cell, mat-cell {
justify-content: center;
}
.one-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon-button-spinner {
position: absolute;
top: 7px;
left: 6px;
}
.downloads-action-button-div {
margin-top: 10px;
margin-left: 5px;
}
.rounded-top {
border-radius: 16px 16px 0px 0px !important;
}
.rounded-bottom {
border-radius: 0px 0px 16px 16px !important;
}
.rounded {
border-radius: 16px 16px 16px 16px !important;
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DownloadsComponent } from './downloads.component';
@@ -6,7 +6,7 @@ describe('DownloadsComponent', () => {
let component: DownloadsComponent;
let fixture: ComponentFixture<DownloadsComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ DownloadsComponent ]
})

View File

@@ -1,7 +1,13 @@
import { Component, OnInit, ViewChildren, QueryList, ElementRef, OnDestroy } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChild, Input, EventEmitter } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
import { Router } from '@angular/router';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
import { MatSort } from '@angular/material/sort';
import { Clipboard } from '@angular/cdk/clipboard';
@Component({
selector: 'app-downloads',
@@ -34,138 +40,222 @@ import { Router } from '@angular/router';
})
export class DownloadsComponent implements OnInit, OnDestroy {
@Input() uids = null;
downloads_check_interval = 1000;
downloads = {};
downloads = [];
finished_downloads = [];
interval_id = null;
keys = Object.keys;
valid_sessions_length = 0;
paused_download_exists = false;
running_download_exists = false;
STEP_INDEX_TO_LABEL = {
0: $localize`Creating download`,
1: $localize`Getting info`,
2: $localize`Downloading file`,
3: $localize`Complete`
}
displayedColumns: string[] = ['date', 'title', 'stage', 'subscription', 'progress', 'actions'];
dataSource = null; // new MatTableDataSource<Download>();
downloads_retrieved = false;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
sort_downloads = (a, b) => {
const result = b.value.timestamp_start - a.value.timestamp_start;
const result = b.timestamp_start - a.timestamp_start;
return result;
}
constructor(public postsService: PostsService, private router: Router) { }
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog, private clipboard: Clipboard) { }
ngOnInit(): void {
if (this.postsService.initialized) {
this.getCurrentDownloadsRecurring();
} else {
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getCurrentDownloadsRecurring();
}
});
}
}
getCurrentDownloadsRecurring(): void {
if (!this.postsService.config['Extra']['enable_downloads_manager']) {
this.router.navigate(['/home']);
return;
}
this.getCurrentDownloads();
this.interval_id = setInterval(() => {
this.getCurrentDownloads();
}, this.downloads_check_interval);
this.postsService.service_initialized.subscribe(init => {
if (init) {
if (!this.postsService.config['Extra']['enable_downloads_manager']) {
this.router.navigate(['/home']);
}
}
});
}
ngOnDestroy() {
ngOnDestroy(): void {
if (this.interval_id) { clearInterval(this.interval_id) }
}
getCurrentDownloads() {
this.postsService.getCurrentDownloads().subscribe(res => {
if (res['downloads']) {
this.assignNewValues(res['downloads']);
getCurrentDownloads(): void {
this.postsService.getCurrentDownloads(this.uids).subscribe(res => {
this.downloads_retrieved = true;
if (res['downloads'] !== null
&& res['downloads'] !== undefined
&& JSON.stringify(this.downloads) !== JSON.stringify(res['downloads'])) {
this.downloads = this.combineDownloads(this.downloads, res['downloads']);
// this.downloads = res['downloads'];
this.downloads.sort(this.sort_downloads);
this.dataSource = new MatTableDataSource<Download>(this.downloads);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.paused_download_exists = this.downloads.find(download => download['paused'] && !download['error']);
this.running_download_exists = this.downloads.find(download => !download['paused'] && !download['finished']);
} else {
// failed to get downloads
}
});
}
clearDownload(session_id, download_uid) {
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
if (res['success']) {
// this.downloads = res['downloads'];
} else {
clearFinishedDownloads(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: $localize`Clear finished downloads`,
dialogText: $localize`Would you like to clear your finished downloads?`,
submitText: $localize`Clear`,
warnSubmitColor: true
}
});
}
clearDownloads(session_id) {
this.postsService.clearDownloads(false, session_id).subscribe(res => {
if (res['success']) {
this.downloads = res['downloads'];
} else {
}
});
}
clearAllDownloads() {
this.postsService.clearDownloads(true).subscribe(res => {
if (res['success']) {
this.downloads = res['downloads'];
} else {
}
});
}
assignNewValues(new_downloads_by_session) {
const session_keys = Object.keys(new_downloads_by_session);
// remove missing session IDs
const current_session_ids = Object.keys(this.downloads);
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
for (const missing_session_id of missing_session_ids) {
delete this.downloads[missing_session_id];
}
// loop through sessions
for (let i = 0; i < session_keys.length; i++) {
const session_id = session_keys[i];
const session_downloads_by_id = new_downloads_by_session[session_id];
const session_download_ids = Object.keys(session_downloads_by_id);
if (this.downloads[session_id]) {
// remove missing download IDs
const current_download_ids = Object.keys(this.downloads[session_id]);
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
for (const missing_download_id of missing_download_ids) {
console.log('removing missing download id');
delete this.downloads[session_id][missing_download_id];
}
}
if (!this.downloads[session_id]) {
this.downloads[session_id] = session_downloads_by_id;
} else {
for (let j = 0; j < session_download_ids.length; j++) {
const download_id = session_download_ids[j];
const download = new_downloads_by_session[session_id][download_id]
if (!this.downloads[session_id][download_id]) {
this.downloads[session_id][download_id] = download;
} else {
const download_to_update = this.downloads[session_id][download_id];
download_to_update['percent_complete'] = download['percent_complete'];
download_to_update['complete'] = download['complete'];
download_to_update['timestamp_end'] = download['timestamp_end'];
download_to_update['downloading'] = download['downloading'];
download_to_update['error'] = download['error'];
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.postsService.clearFinishedDownloads().subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to clear finished downloads!');
}
}
});
}
});
}
pauseDownload(download_uid: string): void {
this.postsService.pauseDownload(download_uid).subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.');
}
});
}
pauseAllDownloads(): void {
this.postsService.pauseAllDownloads().subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to pause all downloads! See server logs for more info.');
}
});
}
resumeDownload(download_uid: string): void {
this.postsService.resumeDownload(download_uid).subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to resume download! See server logs for more info.');
}
});
}
resumeAllDownloads(): void {
this.postsService.resumeAllDownloads().subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to resume all downloads! See server logs for more info.');
}
});
}
restartDownload(download_uid: string): void {
this.postsService.restartDownload(download_uid).subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to restart download! See server logs for more info.');
}
});
}
cancelDownload(download_uid: string): void {
this.postsService.cancelDownload(download_uid).subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to cancel download! See server logs for more info.');
}
});
}
clearDownload(download_uid: string): void {
this.postsService.clearDownload(download_uid).subscribe(res => {
if (!res['success']) {
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.');
}
});
}
watchContent(download): void {
const container = download['container'];
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
const is_playlist = container['uids']; // hacky, TODO: fix
if (is_playlist) {
this.router.navigate(['/player', {playlist_id: container['id'], type: download['type']}]);
} else {
this.router.navigate(['/player', {type: download['type'], uid: container['uid']}]);
}
}
downloadsValid() {
let valid = false;
const keys = this.keys(this.downloads);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = this.downloads[key];
if (this.keys(value).length > 0) {
valid = true;
break;
}
}
return valid;
combineDownloads(downloads_old, downloads_new) {
// only keeps downloads that exist in the new set
downloads_old = downloads_old.filter(download_old => downloads_new.some(download_new => download_new.uid === download_old.uid));
// add downloads from the new set that the old one doesn't have
const downloads_to_add = downloads_new.filter(download_new => !downloads_old.some(download_old => download_new.uid === download_old.uid));
downloads_old.push(...downloads_to_add);
downloads_old.forEach(download_old => {
const download_new = downloads_new.find(download_to_check => download_old.uid === download_to_check.uid);
Object.keys(download_new).forEach(key => {
download_old[key] = download_new[key];
});
Object.keys(download_old).forEach(key => {
if (!download_new[key]) delete download_old[key];
});
});
return downloads_old;
}
showError(download) {
const copyToClipboardEmitter = new EventEmitter<boolean>();
this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: $localize`Error for ${download['url']}:url:`,
dialogText: download['error'],
submitText: $localize`Copy to clipboard`,
cancelText: $localize`Close`,
closeOnSubmit: false,
onlyEmitOnDone: true,
doneEmitter: copyToClipboardEmitter
}
});
copyToClipboardEmitter.subscribe(done => {
if (done) {
this.postsService.openSnackBar($localize`Copied to clipboard!`);
this.clipboard.copy(download['error']);
}
});
}
}
export interface Download {
timestamp_start: number;
title: string;
step_index: number;
progress: string;
}

View File

@@ -1,5 +1,5 @@
<mat-card class="login-card">
<mat-tab-group [(selectedIndex)]="selectedTabIndex">
<mat-tab-group style="margin-bottom: 20px" [(selectedIndex)]="selectedTabIndex">
<mat-tab label="Login">
<div style="margin-top: 10px;">
<mat-form-field>
@@ -11,9 +11,6 @@
<input [(ngModel)]="loginPasswordInput" (keyup.enter)="login()" type="password" matInput placeholder="Password">
</mat-form-field>
</div>
<div style="margin-bottom: 10px; margin-top: 10px;">
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
</div>
</mat-tab>
<mat-tab *ngIf="registrationEnabled" label="Register">
<div style="margin-top: 10px;">
@@ -31,9 +28,14 @@
<input [(ngModel)]="registrationPasswordConfirmationInput" type="password" matInput placeholder="Confirm Password">
</mat-form-field>
</div>
<div style="margin-bottom: 10px; margin-top: 10px;">
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
</div>
</mat-tab>
</mat-tab-group>
<div *ngIf="selectedTabIndex === 0" class="login-button-div">
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
<mat-progress-bar *ngIf="loggingIn" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
</div>
<div *ngIf="selectedTabIndex === 1" class="login-button-div">
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
<mat-progress-bar *ngIf="registering" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
</div>
</mat-card>

View File

@@ -1,6 +1,33 @@
.login-card {
max-width: 600px;
max-width: 400px;
width: 80%;
margin: 0 auto;
margin-top: 20px;
padding-top: 8px;
}
.login-div {
height: calc(100% - 170px);
overflow-y: auto;
}
.login-button-div {
margin-bottom: 10px;
margin-top: 10px;
margin-left: -16px;
margin-right: -16px;
bottom: 0px;
width: 100%;
position: absolute;
}
.login-button-div > button {
width: 100%;
border-radius: 0px 0px 4px 4px !important;
}
.login-progress-bar {
position: absolute;
bottom: 0px;
border-radius: 0px 0px 4px 4px;
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LoginComponent } from './login.component';
@@ -6,7 +6,7 @@ describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})

View File

@@ -27,7 +27,7 @@ export class LoginComponent implements OnInit {
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
ngOnInit(): void {
if (this.postsService.isLoggedIn) {
if (this.postsService.isLoggedIn && localStorage.getItem('jwt_token') !== 'null') {
this.router.navigate(['/home']);
}
this.postsService.service_initialized.subscribe(init => {

View File

@@ -1,4 +1,4 @@
<div style="height: 275px;">
<div style="height: 100%;">
<div *ngIf="logs_loading" style="z-index: 999; position: absolute; top: 40%; left: 50%">
<mat-spinner [diameter]="32"></mat-spinner>
</div>
@@ -10,7 +10,7 @@
</cdk-virtual-scroll-viewport>-->
<!-- Non-virtual mode (slow, bug-free) -->
<div style="height: 274px; overflow-y: auto">
<div style="height: 100%; overflow-y: auto">
<div *ngFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LogsViewerComponent } from './logs-viewer.component';
@@ -6,7 +6,7 @@ describe('LogsViewerComponent', () => {
let component: LogsViewerComponent;
let fixture: ComponentFixture<LogsViewerComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LogsViewerComponent ]
})

View File

@@ -61,7 +61,8 @@ export class LogsViewerComponent implements OnInit {
data: {
dialogTitle: 'Clear logs',
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
submitText: 'Clear'
submitText: 'Clear',
warnSubmitColor: true
}
});
dialogRef.afterClosed().subscribe(confirmed => {

View File

@@ -5,7 +5,7 @@
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
<span matLine>
<mat-radio-group [disabled]="permission === 'settings' && role.name === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ManageRoleComponent } from './manage-role.component';
@@ -6,7 +6,7 @@ describe('ManageRoleComponent', () => {
let component: ManageRoleComponent;
let fixture: ComponentFixture<ManageRoleComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ManageRoleComponent ]
})

View File

@@ -47,7 +47,7 @@ export class ManageRoleComponent implements OnInit {
}
changeRolePermissions(change, permission) {
this.postsService.setRolePermission(this.role.name, permission, change.value).subscribe(res => {
this.postsService.setRolePermission(this.role.key, permission, change.value).subscribe(res => {
if (res['success']) {
} else {

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ManageUserComponent } from './manage-user.component';
@@ -6,7 +6,7 @@ describe('ManageUserComponent', () => {
let component: ManageUserComponent;
let fixture: ComponentFixture<ManageUserComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ManageUserComponent ]
})

View File

@@ -1,7 +1,7 @@
<div *ngIf="dataSource; else loading">
<div style="padding: 15px">
<div class="row">
<div class="table table-responsive px-5 pb-4 pt-2">
<div class="table table-responsive pb-4 pt-2">
<div class="example-header">
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search" i18n-placeholder="search field description">
@@ -94,7 +94,7 @@
</div>
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
<mat-menu #edit_roles_menu="matMenu">
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.name}}</button>
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.key}}</button>
</mat-menu>
</div>

View File

@@ -1,5 +1,4 @@
.edit-role {
position: relative;
top: -50px;
left: 35px;
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ModifyUsersComponent } from './modify-users.component';
@@ -6,7 +6,7 @@ describe('ModifyUsersComponent', () => {
let component: ModifyUsersComponent;
let fixture: ComponentFixture<ModifyUsersComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ModifyUsersComponent ]
})

View File

@@ -78,16 +78,7 @@ export class ModifyUsersComponent implements OnInit, AfterViewInit {
getRoles() {
this.postsService.getRoles().subscribe(res => {
this.roles = [];
const roles = res['roles'];
const role_names = Object.keys(roles);
for (let i = 0; i < role_names.length; i++) {
const role_name = role_names[i];
this.roles.push({
name: role_name,
permissions: roles[role_name]['permissions']
});
}
this.roles = res['roles'];
});
}

View File

@@ -28,18 +28,38 @@
</div>
</div>
<div>
<div class="container">
<div class="container" style="margin-bottom: 16px">
<div class="row justify-content-center">
<ng-container *ngIf="normal_files_received">
<div *ngFor="let file of filtered_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' : '' ]">
<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']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
<ng-container *ngIf="normal_files_received && paged_data">
<div *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 [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 ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
</div>
<div *ngIf="paged_data.length === 0">
<ng-container i18n="No videos found">No videos 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' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
<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>
</div>
</div>
<div>
<div style="position: absolute; margin-left: -8px; margin-top: 5px; scale: 0.8">
<mat-form-field>
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>
<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 *ngIf="paged_data && paged_data.length > 0" (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]">
</mat-paginator>
</div>
</div>

View File

@@ -47,6 +47,10 @@
top: 10px;
}
.paginator {
margin-top: 5px;
}
.my-videos-title {
text-align: center;
position: relative;

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RecentVideosComponent } from './recent-videos.component';
@@ -6,7 +6,7 @@ describe('RecentVideosComponent', () => {
let component: RecentVideosComponent;
let fixture: ComponentFixture<RecentVideosComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ RecentVideosComponent ]
})

View File

@@ -1,7 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
import { FileType } from '../../../api-types';
import { MatPaginator } from '@angular/material/paginator';
import { Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'app-recent-videos',
@@ -15,8 +18,8 @@ export class RecentVideosComponent implements OnInit {
normal_files_received = false;
subscription_files_received = false;
files: any[] = null;
filtered_files: any[] = null;
file_count = 10;
searchChangedSubject: Subject<string> = new Subject<string>();
downloading_content = {'video': {}, 'audio': {}};
search_mode = false;
search_text = '';
@@ -50,11 +53,20 @@ export class RecentVideosComponent implements OnInit {
}
};
filterProperty = this.filterProperties['upload_date'];
fileTypeFilter = 'both';
playlists = null;
pageSize = 10;
paged_data = null;
@ViewChild('paginator') paginator: MatPaginator
constructor(public postsService: PostsService, private router: Router) {
// get cached file count
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count');
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
this.loading_files = Array(this.cached_file_count).fill(0);
}
}
@@ -62,78 +74,108 @@ export class RecentVideosComponent implements OnInit {
ngOnInit(): void {
if (this.postsService.initialized) {
this.getAllFiles();
this.getAllPlaylists();
}
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllFiles();
this.getAllPlaylists();
}
});
this.postsService.files_changed.subscribe(changed => {
if (changed) {
this.getAllFiles();
}
});
// set filter property to cached
this.postsService.playlists_changed.subscribe(changed => {
if (changed) {
this.getAllPlaylists();
}
});
// set filter property to cached value
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
// set file type filter to cached value
const cached_file_type_filter = localStorage.getItem('file_type_filter');
if (cached_file_type_filter) {
this.fileTypeFilter = cached_file_type_filter;
}
const sort_order = localStorage.getItem('recent_videos_sort_order');
if (sort_order) {
this.descendingMode = sort_order === 'descending';
}
this.searchChangedSubject
.debounceTime(500)
.pipe(distinctUntilChanged()
).subscribe(model => {
if (model.length > 0) {
this.search_mode = true;
} else {
this.search_mode = false;
}
this.getAllFiles();
});
}
getAllPlaylists() {
this.postsService.getPlaylists().subscribe(res => {
this.playlists = res['playlists'];
});
}
// 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));
}
this.normal_files_received = false;
this.searchChangedSubject.next(newvalue);
}
filterOptionChanged(value) {
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
this.getAllFiles();
}
fileTypeFilterChanged(value) {
localStorage.setItem('file_type_filter', value);
this.getAllFiles();
}
toggleModeChange() {
this.descendingMode = !this.descendingMode;
this.filterByProperty(this.filterProperty['property']);
localStorage.setItem('recent_videos_sort_order', this.descendingMode ? 'descending' : 'ascending');
this.getAllFiles();
}
// get files
getAllFiles() {
this.normal_files_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.files = res['files'];
this.files.forEach(file => {
getAllFiles(cache_mode = false) {
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 range = [current_file_index, current_file_index + this.pageSize];
this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter).subscribe(res => {
this.file_count = res['file_count'];
this.paged_data = res['files'];
for (let i = 0; i < this.paged_data.length; i++) {
const file = this.paged_data[i];
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']);
// set cached file count for future use, note that we convert the amount of files to a string
localStorage.setItem('cached_file_count', '' + this.files.length);
localStorage.setItem('cached_file_count', '' + this.file_count);
this.normal_files_received = true;
});
}
@@ -155,15 +197,14 @@ export class RecentVideosComponent implements OnInit {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) {
// streaming only mode subscriptions
!new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
// !new_tab ? this.router.navigate(['/player', {name: file.id,
// url: file.requested_formats ? file.requested_formats[0].url : file.url}])
// : window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else {
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
!new_tab ? this.router.navigate(['/player', {uid: file.uid,
type: file.isAudio ? 'audio' : 'video'}])
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'}`);
}
} else {
// normal files
@@ -190,15 +231,12 @@ export class RecentVideosComponent implements OnInit {
const type = (file.isAudio ? 'audio' : 'video') as FileType;
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.downloadFileFromServer(
file.id, type,
{subscriptionName: sub.name, subPlaylist: sub.isPlaylist, uid: this.postsService.user ? this.postsService.user.uid : null}
).subscribe(res => {
const blob: Blob = res;
saveAs(blob, file.id + ext);
}, err => {
console.log(err);
});
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
const blob: Blob = res;
saveAs(blob, file.id + ext);
}, err => {
console.log(err);
});
}
downloadNormalFile(file) {
@@ -206,14 +244,14 @@ export class RecentVideosComponent implements OnInit {
const ext = type === 'audio' ? '.mp3' : '.mp4'
const name = file.id;
this.downloading_content[type][name] = true;
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
this.postsService.downloadFileFromServer(file.uid).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 => {
this.postsService.deleteFile(file.uid).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
@@ -229,17 +267,17 @@ export class RecentVideosComponent implements OnInit {
const blacklistMode = args.blacklistMode;
if (file.sub_id) {
this.deleteSubscriptionFile(file, index, blacklistMode);
this.deleteSubscriptionFile(file, blacklistMode);
} else {
this.deleteNormalFile(file, index, blacklistMode);
this.deleteNormalFile(file, blacklistMode);
}
}
deleteNormalFile(file, index, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
deleteNormalFile(file, blacklistMode = false) {
this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.files.splice(index, 1);
this.removeFileCard(file);
} else {
this.postsService.openSnackBar('Delete failed!', 'OK.');
}
@@ -248,27 +286,50 @@ export class RecentVideosComponent implements OnInit {
});
}
deleteSubscriptionFile(file, index, blacklistMode = false) {
deleteSubscriptionFile(file, blacklistMode = false) {
if (blacklistMode) {
this.deleteForever(file, index);
this.deleteForever(file);
} else {
this.deleteAndRedownload(file, index);
this.deleteAndRedownload(file);
}
}
deleteAndRedownload(file, index) {
deleteAndRedownload(file) {
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);
this.removeFileCard(file);
});
}
deleteForever(file, index) {
deleteForever(file) {
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);
this.removeFileCard(file);
});
}
removeFileCard(file_to_remove) {
const index = this.paged_data.map(e => e.uid).indexOf(file_to_remove.uid);
this.paged_data.splice(index, 1);
this.getAllFiles(true);
}
addFileToPlaylist(info_obj) {
const file = info_obj['file'];
const playlist_id = info_obj['playlist_id'];
const playlist = this.playlists.find(potential_playlist => potential_playlist['id'] === playlist_id);
this.postsService.addFileToPlaylist(playlist_id, file['uid']).subscribe(res => {
if (res['success']) {
this.postsService.openSnackBar(`Successfully added ${file.title} to ${playlist.title}!`);
this.postsService.playlists_changed.next(true);
} else {
this.postsService.openSnackBar(`Failed to add ${file.title} to ${playlist.title}! Unknown error.`);
}
}, err => {
console.error(err);
this.postsService.openSnackBar(`Failed to add ${file.title} to ${playlist.title}! See browser console for error.`);
});
}
@@ -283,9 +344,15 @@ export class RecentVideosComponent implements OnInit {
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));
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;
}
pageChangeEvent(event) {
this.pageSize = event.pageSize;
this.loading_files = Array(this.pageSize).fill(0);
this.getAllFiles();
}
}

View File

@@ -0,0 +1,11 @@
<span class="text" [ngStyle]="{'-webkit-line-clamp': !see_more_active ? line_limit : null}" [innerHTML]="text | linkify"></span>
<span>
<a [routerLink]="" (click)="toggleSeeMore()">
<ng-container *ngIf="!see_more_active" i18n="See more">
See more.
</ng-container>
<ng-container *ngIf="see_more_active" i18n="See less">
See less.
</ng-container>
</a>
</span>

View File

@@ -0,0 +1,7 @@
.text {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
word-wrap: break-word;
}

View File

@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FileCardComponent } from './file-card.component';
import { SeeMoreComponent } from './see-more.component';
describe('FileCardComponent', () => {
let component: FileCardComponent;
let fixture: ComponentFixture<FileCardComponent>;
describe('SeeMoreComponent', () => {
let component: SeeMoreComponent;
let fixture: ComponentFixture<SeeMoreComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FileCardComponent ]
declarations: [ SeeMoreComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileCardComponent);
fixture = TestBed.createComponent(SeeMoreComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,60 @@
import { Component, Input, OnInit, Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Pipe({ name: 'linkify' })
export class LinkifyPipe implements PipeTransform {
constructor(private _domSanitizer: DomSanitizer) {}
transform(value: any, args?: any): any {
return this._domSanitizer.bypassSecurityTrustHtml(this.stylize(value));
}
// Modify this method according to your custom logic
private stylize(text: string): string {
let stylizedText: string = '';
if (text && text.length > 0) {
for (let line of text.split("\n")) {
for (let t of line.split(" ")) {
if (t.startsWith("http") && t.length>7) {
stylizedText += `<a target="_blank" href="${t}">${t}</a> `;
}
else
stylizedText += t + " ";
}
stylizedText += '<br>';
}
return stylizedText;
}
else return text;
}
}
@Component({
selector: 'app-see-more',
templateUrl: './see-more.component.html',
providers: [LinkifyPipe],
styleUrls: ['./see-more.component.scss']
})
export class SeeMoreComponent implements OnInit {
see_more_active = false;
@Input() text = '';
@Input() line_limit = 2;
constructor() { }
ngOnInit(): void {
}
toggleSeeMore() {
this.see_more_active = !this.see_more_active;
}
parseText() {
return this.text.replace(/(http.*?\s)/, "<a href=\"$1\">$1</a>")
}
}

View File

@@ -0,0 +1 @@
<button *ngIf="show_skip_ad_button" (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>

View File

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

View File

@@ -0,0 +1,115 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PostsService } from 'app/posts.services';
import CryptoJS from 'crypto-js';
@Component({
selector: 'app-skip-ad-button',
templateUrl: './skip-ad-button.component.html',
styleUrls: ['./skip-ad-button.component.scss']
})
export class SkipAdButtonComponent implements OnInit {
@Input() current_video = null;
@Input() playback_timestamp = null;
@Output() setPlaybackTimestamp = new EventEmitter<any>();
sponsor_block_cache = {};
show_skip_ad_button = false;
skip_ad_button_check_interval = null;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.skip_ad_button_check_interval = setInterval(() => this.skipAdButtonCheck(), 500);
}
ngOnDestroy(): void {
clearInterval(this.skip_ad_button_check_interval);
}
checkSponsorBlock(video_to_check) {
if (!video_to_check) return;
// check cache, null means it has been checked and confirmed not to exist (limits API calls)
if (this.sponsor_block_cache[video_to_check.url] || this.sponsor_block_cache[video_to_check.url] === null) return;
// sponsor block needs first 4 chars from video ID hash
const video_id = this.getVideoIDFromURL(video_to_check.url);
const id_hash = this.getVideoIDHashFromURL(video_id);
if (!id_hash || id_hash.length < 4) return;
const truncated_id_hash = id_hash.substring(0, 4);
// we couldn't get the data from the cache, let's get it from sponsor block directly
this.postsService.getSponsorBlockDataForVideo(truncated_id_hash).subscribe(res => {
if (res && res['length'] && res['length'] === 0) {
return;
}
const found_data = res['find'](data => data['videoID'] === video_id);
if (found_data) {
this.sponsor_block_cache[video_to_check.url] = found_data;
} else {
this.sponsor_block_cache[video_to_check.url] = null;
}
}, err => {
// likely doesn't exist
this.sponsor_block_cache[video_to_check.url] = null;
});
}
getVideoIDHashFromURL(video_id) {
if (!video_id) return null;
return CryptoJS.SHA256(video_id).toString(CryptoJS.enc.Hex);;
}
getVideoIDFromURL(url) {
const regex_exp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regex_exp);
return (match && match[7].length==11) ? match[7] : null;
}
skipAdButtonCheck() {
const sponsor_block_data = this.sponsor_block_cache[this.current_video.url];
if (!sponsor_block_data && sponsor_block_data !== null) {
// we haven't yet tried to get the sponsor block data for the video
this.checkSponsorBlock(this.current_video);
} else if (!sponsor_block_data) {
this.show_skip_ad_button = false;
return;
}
if (this.getTimeToSkipTo()) {
this.show_skip_ad_button = true;
} else {
this.show_skip_ad_button = false;
}
}
getTimeToSkipTo() {
const sponsor_block_data = this.sponsor_block_cache[this.current_video.url];
if (!sponsor_block_data) return;
// check if we're in between an ad segment
const found_segment = sponsor_block_data['segments'].find(segment_data => this.playback_timestamp > segment_data.segment[0] && this.playback_timestamp < segment_data.segment[1] - 0.5);
if (found_segment) {
return found_segment['segment'][1];
}
return null;
}
skipAdButtonClicked() {
const time_to_skip_to = this.getTimeToSkipTo();
if (!time_to_skip_to) return;
this.setPlaybackTimestamp.emit(time_to_skip_to);
this.show_skip_ad_button = false;
}
}

View File

@@ -0,0 +1,12 @@
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
<div #chat style="max-width: 250px" *ngFor="let chat of visible_chat; let last = last">
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
{{last ? scrollToBottom() : ''}}
</div>
</div>
<ng-container *ngIf="chat_response_received && !full_chat">
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
</ng-container>

View File

@@ -0,0 +1,13 @@
.chat-container {
height: 100%;
overflow-y: scroll;
}
.download-button {
margin: 10px;
}
.downloading-spinner {
top: 50%;
left: 80px;
}

View File

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

View File

@@ -0,0 +1,138 @@
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-twitch-chat',
templateUrl: './twitch-chat.component.html',
styleUrls: ['./twitch-chat.component.scss']
})
export class TwitchChatComponent implements OnInit, AfterViewInit {
full_chat = null;
visible_chat = null;
chat_response_received = false;
downloading_chat = false;
current_chat_index = null;
CHAT_CHECK_INTERVAL_MS = 200;
chat_check_interval_obj = null;
scrollContainer = null;
@Input() db_file = null;
@Input() sub = null;
@Input() current_timestamp = null;
@ViewChild('scrollContainer') scrollRef: ElementRef;
@ViewChildren('chat') chat: QueryList<any>;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getFullChat();
}
ngAfterViewInit() {
}
private isUserNearBottom(): boolean {
const threshold = 150;
const position = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight;
const height = this.scrollContainer.scrollHeight;
return position > height - threshold;
}
scrollToBottom = (force_scroll) => {
if (force_scroll || this.isUserNearBottom()) {
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
}
}
addNewChatMessages() {
const next_chat_index = this.getIndexOfNextChat();
if (!this.scrollContainer) {
this.scrollContainer = this.scrollRef.nativeElement;
}
if (this.current_chat_index === null) {
this.current_chat_index = next_chat_index;
}
if (Math.abs(next_chat_index - this.current_chat_index) > 25) {
this.visible_chat = [];
this.current_chat_index = next_chat_index - 25;
setTimeout(() => this.scrollToBottom(true), 100);
}
const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0;
for (let i = this.current_chat_index + 1; i < this.full_chat.length; i++) {
if (this.full_chat[i]['timestamp'] >= latest_chat_timestamp && this.full_chat[i]['timestamp'] <= this.current_timestamp) {
this.visible_chat.push(this.full_chat[i]);
this.current_chat_index = i;
} else if (this.full_chat[i]['timestamp'] > this.current_timestamp) {
break;
}
}
}
getIndexOfNextChat() {
const index = binarySearch(this.full_chat, 'timestamp', this.current_timestamp);
return index;
}
getFullChat() {
this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null, this.sub).subscribe(res => {
this.chat_response_received = true;
if (res['chat']) {
this.initializeChatCheck(res['chat']);
}
});
}
downloadTwitchChat() {
this.downloading_chat = true;
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
vodId = vodId.split('?')[0];
if (!vodId) {
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"');
}
this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null, this.sub).subscribe(res => {
if (res['chat']) {
this.initializeChatCheck(res['chat']);
} else {
this.downloading_chat = false;
this.postsService.openSnackBar('Download failed.')
}
}, err => {
this.downloading_chat = false;
this.postsService.openSnackBar('Chat could not be downloaded.')
});
}
initializeChatCheck(full_chat) {
this.full_chat = full_chat;
this.visible_chat = [];
this.chat_check_interval_obj = setInterval(() => this.addNewChatMessages(), this.CHAT_CHECK_INTERVAL_MS);
}
}
function binarySearch(arr, key, n) {
let min = 0;
let max = arr.length - 1;
let mid;
while (min <= max) {
// tslint:disable-next-line: no-bitwise
mid = (min + max) >>> 1;
if (arr[mid][key] === n) {
return mid;
} else if (arr[mid][key] < n) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return min;
}

View File

@@ -1,5 +1,10 @@
<div (mouseover)="elevated=true" (mouseout)="elevated=false" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
<div *ngIf="!loading" 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>
<div (mouseenter)="onMouseOver()" (mouseleave)="onMouseOut()" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
<div *ngIf="!loading" class="download-time">
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
&nbsp;&nbsp;
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
</div>
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<!-- The context menu trigger must be kept above the "more info" menu -->
<div style="visibility: hidden; position: fixed"
@@ -7,7 +12,7 @@
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="context_menu">
</div>
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<button *ngIf="!file_obj || !file_obj.auto" [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #context_menu>
<ng-container *ngIf="!loading">
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
@@ -18,6 +23,12 @@
<ng-container *ngIf="!is_playlist && !loading">
<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 *ngIf="availablePlaylists" [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>
<mat-menu #addtoplaylist="matMenu">
<ng-container *ngFor="let playlist of availablePlaylists">
<button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
</ng-container>
</mat-menu>
<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>
@@ -40,8 +51,10 @@
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<div [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" style="position: relative">
<img *ngIf="!hide_image || is_playlist || (file_obj.type === 'audio' || file_obj.isAudio)" [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<video *ngIf="elevated && !is_playlist && !(file_obj.type === 'audio' || file_obj.isAudio)" autoplay loop muted [muted]="true" [ngClass]="{'video-small': card_size === 'small', 'video': card_size === 'medium', 'video-large': card_size === 'large'}" [src]="streamURL">
</video>
<div class="duration-time">
{{file_length}}
</div>

View File

@@ -51,6 +51,30 @@
object-fit: cover;
}
.video-large {
width: 300px;
height: 167.5px;
object-fit: cover;
position: absolute;
top: 0px;
}
.video {
width: 200px;
height: 112.5px;
object-fit: cover;
position: absolute;
top: 0px;
}
.video-small {
width: 150px;
height: 84.5px;
object-fit: cover;
position: absolute;
top: 0px;
}
.example-full-width-height {
width: 100%;
height: 100%
@@ -110,7 +134,12 @@
position: absolute;
top: 1px;
left: 5px;
z-index: 99999;
z-index: 999;
width: calc(100% - 8px);
white-space: nowrap;
overflow: hidden;
display: block;
text-overflow: ellipsis;
}
.audio-video-icon {

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UnifiedFileCardComponent } from './unified-file-card.component';
@@ -6,7 +6,7 @@ describe('UnifiedFileCardComponent', () => {
let component: UnifiedFileCardComponent;
let fixture: ComponentFixture<UnifiedFileCardComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ UnifiedFileCardComponent ]
})

View File

@@ -3,6 +3,20 @@ import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { DomSanitizer } from '@angular/platform-browser';
import { MatMenuTrigger } from '@angular/material/menu';
import { registerLocaleData } from '@angular/common';
import localeGB from '@angular/common/locales/en-GB';
import localeFR from '@angular/common/locales/fr';
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';
registerLocaleData(localeGB);
registerLocaleData(localeFR);
registerLocaleData(localeES);
registerLocaleData(localeDE);
registerLocaleData(localeZH);
registerLocaleData(localeNB);
@Component({
selector: 'app-unified-file-card',
@@ -21,6 +35,9 @@ export class UnifiedFileCardComponent implements OnInit {
// optional vars
thumbnailBlobURL = null;
streamURL = null;
hide_image = false;
// input/output
@Input() loading = true;
@Input() theme = null;
@@ -29,11 +46,17 @@ export class UnifiedFileCardComponent implements OnInit {
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Input() locale = null;
@Input() baseStreamPath = null;
@Input() jwtString = null;
@Input() availablePlaylists = null;
@Output() goToFile = new EventEmitter<any>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() addFileToPlaylist = new EventEmitter<any>();
@Output() editPlaylist = new EventEmitter<any>();
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
contextMenuPosition = { x: '0px', y: '0px' };
@@ -51,12 +74,15 @@ export class UnifiedFileCardComponent implements OnInit {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
if (this.file_obj && this.file_obj.thumbnailBlob) {
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
if (this.file_obj && this.file_obj.thumbnailPath) {
this.thumbnailBlobURL = `${this.baseStreamPath}thumbnail/${encodeURIComponent(this.file_obj.thumbnailPath)}${this.jwtString}`;
/*const mime = getMimeByFilename(this.file_obj.thumbnailPath);
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
const bloburl = URL.createObjectURL(blob);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);*/
}
if (this.file_obj) this.streamURL = this.generateStreamURL();
}
emitDeleteFile(blacklistMode = false) {
@@ -67,6 +93,13 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
emitAddFileToPlaylist(playlist_id) {
this.addFileToPlaylist.emit({
file: this.file_obj,
playlist_id: playlist_id
});
}
navigateToFile(event) {
this.goToFile.emit({file: this.file_obj, event: event});
}
@@ -100,6 +133,33 @@ export class UnifiedFileCardComponent implements OnInit {
this.contextMenu.openMenu();
}
generateStreamURL() {
let baseLocation = 'stream/';
let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${this.file_obj['uid']}`;
if (this.jwtString) {
fullLocation += `&jwt=${this.jwtString}`;
}
fullLocation += '&t=,10';
return fullLocation;
}
onMouseOver() {
this.elevated = true;
setTimeout(() => {
if (this.elevated) {
this.hide_image = true;
}
}, 500);
}
onMouseOut() {
this.elevated = false;
this.hide_image = false;
}
}
function fancyTimeFormat(time) {

View File

@@ -1 +1 @@
export const CURRENT_VERSION = 'v4.1';
export const CURRENT_VERSION = 'v4.2';

View File

@@ -19,9 +19,9 @@
<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>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
</mat-select>
</mat-form-field>
<!-- No videos available -->

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CreatePlaylistComponent } from './create-playlist.component';
@@ -6,7 +6,7 @@ describe('CreatePlaylistComponent', () => {
let component: CreatePlaylistComponent;
let fixture: ComponentFixture<CreatePlaylistComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CreatePlaylistComponent ]
})

View File

@@ -51,9 +51,8 @@ export class CreatePlaylistComponent implements OnInit {
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, duration).subscribe(res => {
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
this.create_in_progress = false;
if (res['success']) {
this.dialogRef.close(true);
@@ -78,36 +77,4 @@ 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

@@ -21,6 +21,17 @@
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container></ng-container>
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p>
<p>
<ng-container i18n="Installation type">Installation type:</ng-container>&nbsp;{{postsService.version_info.type}}
<br>
<ng-container *ngIf="postsService.version_info.type === 'docker'">
<ng-container i18n="Docker tag">Docker tag:</ng-container>&nbsp;{{postsService.version_info.tag}}
<br>
</ng-container>
<ng-container i18n="Commit hash">Commit hash:</ng-container>&nbsp;{{postsService.version_info.commit}}
<br>
<ng-container i18n="Build date">Build date:</ng-container>&nbsp;{{postsService.version_info.date}}
</p>
<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>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AboutDialogComponent } from './about-dialog.component';
@@ -6,7 +6,7 @@ describe('AboutDialogComponent', () => {
let component: AboutDialogComponent;
let fixture: ComponentFixture<AboutDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ AboutDialogComponent ]
})

View File

@@ -19,7 +19,7 @@ export class AboutDialogComponent implements OnInit {
sidepanel_mode = this.postsService.sidepanel_mode;
card_size = this.postsService.card_size;
constructor(private postsService: PostsService) { }
constructor(public postsService: PostsService) { }
ngOnInit(): void {
this.getLatestGithubRelease();

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AddUserDialogComponent } from './add-user-dialog.component';
@@ -6,7 +6,7 @@ describe('AddUserDialogComponent', () => {
let component: AddUserDialogComponent;
let fixture: ComponentFixture<AddUserDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ AddUserDialogComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ArgModifierDialogComponent } from './arg-modifier-dialog.component';
@@ -6,7 +6,7 @@ describe('ArgModifierDialogComponent', () => {
let component: ArgModifierDialogComponent;
let fixture: ComponentFixture<ArgModifierDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ArgModifierDialogComponent ]
})

View File

@@ -6,10 +6,13 @@
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<button [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<div class="mat-spinner" *ngIf="submitClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
<span class="spacer"></span>
<button style="float: right;" mat-stroked-button mat-dialog-close>Cancel</button>
<button style="float: right;" mat-stroked-button mat-dialog-close>
<ng-container *ngIf="cancelText">{{cancelText}}</ng-container>
<ng-container *ngIf="!cancelText" i18n="Cancel">Cancel</ng-container>
</button>
</mat-dialog-actions>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ConfirmDialogComponent } from './confirm-dialog.component';
@@ -6,7 +6,7 @@ describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ConfirmDialogComponent ]
})

View File

@@ -11,16 +11,23 @@ export class ConfirmDialogComponent implements OnInit {
dialogTitle = 'Confirm';
dialogText = 'Would you like to confirm?';
submitText = 'Yes'
cancelText = null;
submitClicked = false;
closeOnSubmit = true;
doneEmitter: EventEmitter<any> = null;
doneEmitter: EventEmitter<boolean> = null;
onlyEmitOnDone = false;
warnSubmitColor = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
if (this.data.submitText) { this.submitText = this.data.submitText };
if (this.data.dialogTitle !== undefined) { this.dialogTitle = this.data.dialogTitle }
if (this.data.dialogText !== undefined) { this.dialogText = this.data.dialogText }
if (this.data.submitText !== undefined) { this.submitText = this.data.submitText }
if (this.data.cancelText !== undefined) { this.cancelText = this.data.cancelText }
if (this.data.warnSubmitColor !== undefined) { this.warnSubmitColor = this.data.warnSubmitColor }
if (this.data.warnSubmitColor !== undefined) { this.warnSubmitColor = this.data.warnSubmitColor }
if (this.data.closeOnSubmit !== undefined) { this.closeOnSubmit = this.data.closeOnSubmit }
// checks if emitter exists, if so don't autoclose as it should be handled by caller
if (this.data.doneEmitter) {
@@ -32,9 +39,9 @@ export class ConfirmDialogComponent implements OnInit {
confirmClicked() {
if (this.onlyEmitOnDone) {
this.doneEmitter.emit(true);
this.submitClicked = true;
if (this.closeOnSubmit) this.submitClicked = true;
} else {
this.dialogRef.close(true);
if (this.closeOnSubmit) this.dialogRef.close(true);
}
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CookiesUploaderDialogComponent } from './cookies-uploader-dialog.component';
@@ -6,7 +6,7 @@ describe('CookiesUploaderDialogComponent', () => {
let component: CookiesUploaderDialogComponent;
let fixture: ComponentFixture<CookiesUploaderDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CookiesUploaderDialogComponent ]
})

View File

@@ -0,0 +1,60 @@
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container>&nbsp;{{category['name']}}</h4>
<mat-dialog-content style="max-height: 50vh">
<mat-form-field style="width: 250px; margin-bottom: 5px;">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="category['name']" required>
</mat-form-field>
<mat-divider></mat-divider>
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
<mat-list>
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
<mat-option value="or">OR</mat-option>
<mat-option value="and">AND</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="property-select">
<mat-select [(ngModel)]="rule['property']">
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="comparator-select">
<mat-select [(ngModel)]="rule['comparator']">
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</mat-list-item>
</mat-list>
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
<mat-divider></mat-divider>
<mat-form-field style="width: 250px; margin-top: 10px;">
<input matInput [(ngModel)]="category['custom_output']" placeholder="Custom file output" i18n-placeholder="Category custom file output placeholder">
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -0,0 +1,16 @@
.operator-select {
width: 55px;
}
.property-select {
margin-left: 10px;
width: 110px;
}
.comparator-select {
margin-left: 10px;
}
.value-input {
margin-left: 10px;
}

View File

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

View File

@@ -0,0 +1,111 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-edit-category-dialog',
templateUrl: './edit-category-dialog.component.html',
styleUrls: ['./edit-category-dialog.component.scss']
})
export class EditCategoryDialogComponent implements OnInit {
updating = false;
original_category = null;
category = null;
propertyOptions = [
{
value: 'fulltitle',
label: 'Title'
},
{
value: 'id',
label: 'ID'
},
{
value: 'webpage_url',
label: 'URL'
},
{
value: 'view_count',
label: 'Views'
},
{
value: 'uploader',
label: 'Uploader'
},
{
value: '_filename',
label: 'File Name'
},
{
value: 'tags',
label: 'Tags'
}
];
comparatorOptions = [
{
value: 'includes',
label: 'includes'
},
{
value: 'not_includes',
label: 'not includes'
},
{
value: 'equals',
label: 'equals'
},
{
value: 'not_equals',
label: 'not equals'
},
];
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) {
if (this.data) {
this.original_category = this.data.category;
this.category = JSON.parse(JSON.stringify(this.original_category));
}
}
ngOnInit(): void {
}
addNewRule() {
this.category['rules'].push({
preceding_operator: 'or',
property: 'fulltitle',
comparator: 'includes',
value: ''
});
}
saveClicked() {
this.updating = true;
this.postsService.updateCategory(this.category).subscribe(res => {
this.updating = false;
this.original_category = JSON.parse(JSON.stringify(this.category));
this.postsService.reloadCategories();
}, err => {
this.updating = false;
console.error(err);
});
}
categoryChanged() {
return JSON.stringify(this.category) === JSON.stringify(this.original_category);
}
swapRules(original_index, new_index) {
[this.category.rules[original_index], this.category.rules[new_index]] = [this.category.rules[new_index],
this.category.rules[original_index]];
}
removeRule(index) {
this.category['rules'].splice(index, 1);
}
}

View File

@@ -1,8 +1,11 @@
<h4 mat-dialog-title i18n="Edit subscription dialog title prefix">Editing</h4>&nbsp;{{sub.name}}
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container>&nbsp;{{sub.name}}&nbsp;<ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox [(ngModel)]="new_sub.paused"><ng-container i18n="Paused subscription setting">Paused</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-3">
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
@@ -24,7 +27,14 @@
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12">
<div class="col-12 mt-2">
<mat-form-field>
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mt-1">
<div>
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
</div>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EditSubscriptionDialogComponent } from './edit-subscription-dialog.component';
@@ -6,7 +6,7 @@ describe('EditSubscriptionDialogComponent', () => {
let component: EditSubscriptionDialogComponent;
let fixture: ComponentFixture<EditSubscriptionDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ EditSubscriptionDialogComponent ]
})

View File

@@ -22,6 +22,36 @@ export class EditSubscriptionDialogComponent implements OnInit {
audioOnlyMode = null;
download_all = null;
available_qualities = [
{
'label': 'Best',
'value': 'best'
},
{
'label': '4K',
'value': '2160'
},
{
'label': '1440p',
'value': '1440'
},
{
'label': '1080p',
'value': '1080'
},
{
'label': '720p',
'value': '720'
},
{
'label': '480p',
'value': '480'
},
{
'label': '360p',
'value': '360'
}
];
time_units = [
'day',
@@ -31,24 +61,24 @@ export class EditSubscriptionDialogComponent implements OnInit {
];
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialog: MatDialog, private postsService: PostsService) {
this.sub = this.data.sub;
this.sub = JSON.parse(JSON.stringify(this.data.sub));
this.new_sub = JSON.parse(JSON.stringify(this.sub));
// ignore videos to keep requests small
delete this.sub['videos'];
delete this.new_sub['videos'];
this.audioOnlyMode = this.sub.type === 'audio';
this.download_all = !this.sub.timerange;
if (this.sub.timerange) {
const timerange_str = this.sub.timerange.split('-')[1];
console.log(timerange_str);
const number = timerange_str.replace(/\D/g,'');
let units = timerange_str.replace(/[0-9]/g, '');
console.log(units);
// // remove plural on units
// if (units[units.length-1] === 's') {
// units = units.substring(0, units.length-1);
// }
if (+number === 1) {
units = units.replace('s', '');
}
this.timerange_amount = parseInt(number);
this.timerange_unit = units;
@@ -71,9 +101,10 @@ export class EditSubscriptionDialogComponent implements OnInit {
}
saveSubscription() {
this.postsService.updateSubscription(this.sub).subscribe(res => {
this.postsService.updateSubscription(this.new_sub).subscribe(res => {
this.sub = this.new_sub;
this.new_sub = JSON.parse(JSON.stringify(this.sub));
this.postsService.reloadSubscriptions();
})
}
@@ -85,12 +116,16 @@ export class EditSubscriptionDialogComponent implements OnInit {
}
timerangeChanged(value, select_changed) {
console.log(this.timerange_amount);
console.log(this.timerange_unit);
if (+this.timerange_amount === 1) {
this.timerange_unit = this.timerange_unit.replace('s', '');
} else {
if (!this.timerange_unit.includes('s')) {
this.timerange_unit += 's';
}
}
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
console.log(this.new_sub.timerange);
} else {
this.new_sub.timerange = null;
}

View File

@@ -1,28 +1,44 @@
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<mat-dialog-content>
<!-- Playlist info -->
<div>
<mat-form-field color="accent">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
</mat-form-field>
</div>
<div *ngIf="playlist">
<!-- Playlist info -->
<div>
<mat-form-field color="accent">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
</mat-form-field>
</div>
<!-- Playlist order -->
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of playlist.fileNames; let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<div>
<mat-checkbox [(ngModel)]="playlist.randomize_order"><ng-container i18n="Randomize order when playing checkbox label">Randomize order when playing</ng-container></mat-checkbox>
</div>
<div class="add-content-button">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add more content">Add more content</ng-container></button>
<div style="margin-bottom: 10px; height: 40px;">
<div style="float: left">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
<div style="float: right">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
</div>
</div>
<!-- Playlist order -->
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist_file_objs.slice().reverse() : playlist_file_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item.title}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file.title}}</button>
</mat-menu>
</div>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
</mat-menu>
</mat-dialog-content>
<mat-dialog-actions>
<!-- Save -->
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
<button [disabled]="!playlist || !playlistChanged()" (click)="updatePlaylist()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
</mat-dialog-actions>

View File

@@ -30,11 +30,6 @@ border: none;
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.add-content-button {
margin-top: 15px;
margin-bottom: 10px;
}
.remove-item-button {
right: 10px;
position: absolute;

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ModifyPlaylistComponent } from './modify-playlist.component';
@@ -6,7 +6,7 @@ describe('ModifyPlaylistComponent', () => {
let component: ModifyPlaylistComponent;
let fixture: ComponentFixture<ModifyPlaylistComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ModifyPlaylistComponent ]
})

View File

@@ -10,11 +10,16 @@ import { PostsService } from 'app/posts.services';
})
export class ModifyPlaylistComponent implements OnInit {
playlist_id = null;
original_playlist = null;
playlist = null;
playlist_file_objs = null;
available_files = [];
all_files = [];
playlist_updated = false;
reverse_order = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
private postsService: PostsService,
@@ -22,10 +27,11 @@ export class ModifyPlaylistComponent implements OnInit {
ngOnInit(): void {
if (this.data) {
this.playlist = JSON.parse(JSON.stringify(this.data.playlist));
this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist));
this.getFiles();
this.playlist_id = this.data.playlist_id;
this.getPlaylist();
}
this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true';
}
getFiles() {
@@ -41,43 +47,61 @@ export class ModifyPlaylistComponent implements OnInit {
}
processFiles(new_files = null) {
if (new_files) { this.all_files = new_files.map(file => file.id); }
this.available_files = this.all_files.filter(e => !this.playlist.fileNames.includes(e))
if (new_files) { this.all_files = new_files; }
this.available_files = this.all_files.filter(e => !this.playlist_file_objs.includes(e))
}
updatePlaylist() {
this.playlist['uids'] = this.playlist_file_objs.map(playlist_file_obj => playlist_file_obj['uid'])
this.postsService.updatePlaylist(this.playlist).subscribe(res => {
this.playlist_updated = true;
this.postsService.openSnackBar('Playlist updated successfully.');
this.getPlaylist();
this.postsService.playlists_changed.next(true);
});
}
playlistChanged() {
return JSON.stringify(this.playlist) === JSON.stringify(this.original_playlist);
return JSON.stringify(this.playlist) !== JSON.stringify(this.original_playlist);
}
getPlaylist() {
this.postsService.getPlaylist(this.playlist.id, this.playlist.type, null).subscribe(res => {
this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => {
if (res['playlist']) {
this.playlist = res['playlist'];
this.playlist_file_objs = res['file_objs'];
this.original_playlist = JSON.parse(JSON.stringify(this.playlist));
this.getFiles();
}
});
}
addContent(file) {
this.playlist.fileNames.push(file);
this.playlist_file_objs.push(file);
this.playlist.uids.push(file.uid);
this.processFiles();
}
removeContent(index) {
this.playlist.fileNames.splice(index, 1);
if (this.reverse_order) {
index = this.playlist_file_objs.length - 1 - index;
}
this.playlist_file_objs.splice(index, 1);
this.playlist.uids.splice(index, 1);
this.processFiles();
}
togglePlaylistOrder() {
this.reverse_order = !this.reverse_order;
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex);
if (this.reverse_order) {
event.previousIndex = this.playlist_file_objs.length - 1 - event.previousIndex;
event.currentIndex = this.playlist_file_objs.length - 1 - event.currentIndex;
}
moveItemInArray(this.playlist_file_objs, event.previousIndex, event.currentIndex);
}
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SetDefaultAdminDialogComponent } from './set-default-admin-dialog.component';
@@ -6,7 +6,7 @@ describe('SetDefaultAdminDialogComponent', () => {
let component: SetDefaultAdminDialogComponent;
let fixture: ComponentFixture<SetDefaultAdminDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ SetDefaultAdminDialogComponent ]
})

View File

@@ -1,7 +1,6 @@
<h4 mat-dialog-title>
<ng-container *ngIf="is_playlist" i18n="Share playlist dialog title">Share playlist</ng-container>
<ng-container *ngIf="!is_playlist && type === 'video'" i18n="Share video dialog title">Share video</ng-container>
<ng-container *ngIf="!is_playlist && type === 'audio'" i18n="Share audio dialog title">Share audio</ng-container>
<ng-container *ngIf="!is_playlist" i18n="Share video dialog title">Share file</ng-container>
</h4>
<mat-dialog-content>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ShareMediaDialogComponent } from './share-media-dialog.component';
@@ -6,7 +6,7 @@ describe('ShareMediaDialogComponent', () => {
let component: ShareMediaDialogComponent;
let fixture: ComponentFixture<ShareMediaDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ShareMediaDialogComponent ]
})

View File

@@ -11,7 +11,6 @@ import { PostsService } from 'app/posts.services';
})
export class ShareMediaDialogComponent implements OnInit {
type = null;
uid = null;
uuid = null;
share_url = null;
@@ -26,14 +25,13 @@ export class ShareMediaDialogComponent implements OnInit {
ngOnInit(): void {
if (this.data) {
this.type = this.data.type;
this.uid = this.data.uid;
this.uuid = this.data.uuid;
this.sharing_enabled = this.data.sharing_enabled;
this.is_playlist = this.data.is_playlist;
this.current_timestamp = (this.data.current_timestamp / 1000).toFixed(2);
const arg = (this.is_playlist ? ';id=' : ';uid=');
const arg = (this.is_playlist ? ';playlist_id=' : ';uid=');
this.default_share_url = window.location.href.split(';')[0] + arg + this.uid;
if (this.uuid) {
this.default_share_url += ';uuid=' + this.uuid;
@@ -65,7 +63,7 @@ export class ShareMediaDialogComponent implements OnInit {
sharingChanged(event) {
if (event.checked) {
this.postsService.enableSharing(this.uid, this.type, this.is_playlist).subscribe(res => {
this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => {
if (res['success']) {
this.openSnackBar('Sharing enabled.');
this.sharing_enabled = true;
@@ -76,7 +74,7 @@ export class ShareMediaDialogComponent implements OnInit {
this.openSnackBar('Failed to enable sharing - server error.');
});
} else {
this.postsService.disableSharing(this.uid, this.type, this.is_playlist).subscribe(res => {
this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => {
if (res['success']) {
this.openSnackBar('Sharing disabled.');
this.sharing_enabled = false;

View File

@@ -35,6 +35,13 @@
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SubscribeDialogComponent } from './subscribe-dialog.component';
@@ -6,7 +6,7 @@ describe('SubscribeDialogComponent', () => {
let component: SubscribeDialogComponent;
let fixture: ComponentFixture<SubscribeDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ SubscribeDialogComponent ]
})

View File

@@ -17,6 +17,8 @@ export class SubscribeDialogComponent implements OnInit {
url = null;
name = null;
maxQuality = 'best';
// state
subscribing = false;
@@ -29,12 +31,43 @@ export class SubscribeDialogComponent implements OnInit {
customFileOutput = '';
customArgs = '';
available_qualities = [
{
'label': 'Best',
'value': 'best'
},
{
'label': '4K',
'value': '2160'
},
{
'label': '1440p',
'value': '1440'
},
{
'label': '1080p',
'value': '1080'
},
{
'label': '720p',
'value': '720'
},
{
'label': '480p',
'value': '480'
},
{
'label': '360p',
'value': '360'
}
];
time_units = [
'day',
'week',
'month',
'year'
]
];
constructor(private postsService: PostsService,
private snackBar: MatSnackBar,
@@ -57,7 +90,7 @@ export class SubscribeDialogComponent implements OnInit {
if (!this.download_all) {
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
}
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode, this.maxQuality,
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
this.subscribing = false;
if (res['new_sub']) {

View File

@@ -1,4 +1,4 @@
<h4 mat-dialog-title>{{sub.name}}</h4>
<h4 mat-dialog-title>{{sub.name}}&nbsp;<ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
<mat-dialog-content>
<div class="info-item">

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SubscriptionInfoDialogComponent } from './subscription-info-dialog.component';
@@ -6,7 +6,7 @@ describe('SubscriptionInfoDialogComponent', () => {
let component: SubscriptionInfoDialogComponent;
let fixture: ComponentFixture<SubscriptionInfoDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ SubscriptionInfoDialogComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UpdateProgressDialogComponent } from './update-progress-dialog.component';
@@ -6,7 +6,7 @@ describe('UpdateProgressDialogComponent', () => {
let component: UpdateProgressDialogComponent;
let fixture: ComponentFixture<UpdateProgressDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ UpdateProgressDialogComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UserProfileDialogComponent } from './user-profile-dialog.component';
@@ -6,7 +6,7 @@ describe('UserProfileDialogComponent', () => {
let component: UserProfileDialogComponent;
let fixture: ComponentFixture<UserProfileDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ UserProfileDialogComponent ]
})

View File

@@ -25,6 +25,10 @@
<div class="info-item-label"><strong><ng-container i18n="Video upload date property">Upload Date:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.upload_date ? file.upload_date : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Category property">Category:</ng-container>&nbsp;</strong></div>
<div class="info-item-value"><ng-container *ngIf="file.category"><mat-chip-list><mat-chip>{{file.category.name}}</mat-chip></mat-chip-list></ng-container><ng-container *ngIf="!file.category">N/A</ng-container></div>
</div>
</mat-dialog-content>
<mat-dialog-actions>

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VideoInfoDialogComponent } from './video-info-dialog.component';
@@ -6,7 +6,7 @@ describe('VideoInfoDialogComponent', () => {
let component: VideoInfoDialogComponent;
let fixture: ComponentFixture<VideoInfoDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ VideoInfoDialogComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DownloadItemComponent } from './download-item.component';
@@ -6,7 +6,7 @@ describe('DownloadItemComponent', () => {
let component: DownloadItemComponent;
let fixture: ComponentFixture<DownloadItemComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ DownloadItemComponent ]
})

View File

@@ -1,68 +0,0 @@
.example-card {
width: 150px;
height: 125px;
padding: 0px;
}
.deleteButton {
top:-5px;
right:-5px;
position:absolute;
}
/* Coerce the <span> icon container away from display:inline */
.mat-icon-button .mat-button-wrapper {
display: flex;
justify-content: center;
}
.image {
width: 100%;
}
.example-full-width-height {
width: 100%;
height: 100%
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.img-div {
height: 60px;
padding: 0px;
margin: 8px 0px 0px -5px;
width: calc(100% + 5px + 5px);
overflow: hidden;
border-radius: 0px 0px 4px 4px;
}
.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;
}
.file-link {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
@media (max-width: 576px){
.example-card {
width: 125px !important;
}
}

View File

@@ -1,29 +0,0 @@
<mat-card class="example-card mat-elevation-z6">
<div style="padding:5px">
<div style="height: 52px;">
<div>
<b><a class="file-link" href="javascript:void(0)" (click)="!playlist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
</div>
<span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container>&nbsp;{{name}}</span>
<div *ngIf="playlist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{count}}</div>
</div>
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
<span *ngIf="!image_loaded">
</span>
</div>
</div>
<button [matMenuTriggerFor]="playlist_menu" *ngIf="playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #playlist_menu="matMenu">
<button (click)="editPlaylistDialog()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</mat-menu>
<button [matMenuTriggerFor]="action_menu" *ngIf="!playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<button (click)="openVideoInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="use_youtubedl_archive" (click)="deleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</mat-menu>
</mat-card>

View File

@@ -1,120 +0,0 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import {PostsService} from '../posts.services';
import { MatSnackBar } from '@angular/material/snack-bar';
import {EventEmitter} from '@angular/core';
import { MainComponent } from 'app/main/main.component';
import { Subject, Observable } from 'rxjs';
import 'rxjs/add/observable/merge';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { ModifyPlaylistComponent } from '../dialogs/modify-playlist/modify-playlist.component';
@Component({
selector: 'app-file-card',
templateUrl: './file-card.component.html',
styleUrls: ['./file-card.component.css']
})
export class FileCardComponent implements OnInit {
@Input() file: any;
@Input() title: string;
@Input() length: string;
@Input() name: string;
@Input() uid: string;
@Input() thumbnailURL: string;
@Input() isAudio = true;
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
@Input() playlist = null;
@Input() count = null;
@Input() use_youtubedl_archive = false;
type;
image_loaded = false;
image_errored = false;
scrollSubject;
scrollAndLoad;
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent,
private dialog: MatDialog) {
this.scrollSubject = new Subject();
this.scrollAndLoad = Observable.merge(
Observable.fromEvent(window, 'scroll'),
this.scrollSubject
);
}
ngOnInit() {
this.type = this.isAudio ? 'audio' : 'video';
if (this.file && this.file.url && this.file.url.includes('youtu')) {
const string_id = (this.playlist ? '?list=' : '?v=')
const index_offset = (this.playlist ? 6 : 3);
const end_index = this.file.url.indexOf(string_id) + index_offset;
this.name = this.file.url.substring(end_index, this.file.url.length);
}
}
deleteFile(blacklistMode = false) {
if (!this.playlist) {
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);
} else {
this.openSnackBar('Delete failed!', 'OK.');
}
}, err => {
this.openSnackBar('Delete failed!', 'OK.');
});
} else {
this.removeFile.emit(this.name);
}
}
openVideoInfoDialog() {
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.file,
},
minWidth: '50vw'
});
}
editPlaylistDialog() {
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: this.playlist,
width: '65vw'
}
});
dialogRef.afterClosed().subscribe(res => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlist = dialogRef.componentInstance.original_playlist;
this.title = this.playlist.name;
this.count = this.playlist.fileNames.length;
}
});
}
onImgError(event) {
this.image_errored = true;
}
onHoverResponse() {
this.scrollSubject.next();
}
imageLoaded(loaded) {
this.image_loaded = true;
}
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -0,0 +1,34 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class H401Interceptor implements HttpInterceptor {
constructor(private router: Router, private snackBar: MatSnackBar) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
localStorage.setItem('jwt_token', null);
if (this.router.url !== '/login' && !this.router.url.includes('player')) {
this.router.navigate(['/login']).then(() => {
this.openSnackBar('Login expired, please login again.');
});
}
}
const error = err.error.message || err.statusText;
return throwError(error);
}));
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { InputDialogComponent } from './input-dialog.component';
@@ -6,7 +6,7 @@ describe('InputDialogComponent', () => {
let component: InputDialogComponent;
let fixture: ComponentFixture<InputDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ InputDialogComponent ]
})

View File

@@ -124,21 +124,40 @@ mat-form-field.mat-form-field {
width: 100%;
}
.edit-button {
.advanced-input-time {
margin-left: 10px;
}
.edit-button {
margin-left: 5px;
margin-top: -6px;
margin-bottom: -5px;
top: -5px;
}
.border-radius-both {
border-radius: 16px;
}
.no-border-radius-bottom {
border-radius: 4px 4px 0px 0px;
border-radius: 16px 16px 0px 0px;
}
.no-border-radius-top {
border-radius: 0px 0px 4px 4px;
border-radius: 0px 0px 16px 16px;
}
@media (max-width: 576px) {
.download-progress-bar {
width: 125px;
}
}
.url-input {
padding-right: 25px;
overflow-y: hidden;
}
.url-input::-webkit-scrollbar {
display: none;
}

View File

@@ -1,6 +1,6 @@
<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 id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : 'border-radius-both'">
<mat-card-content style="padding: 0px 8px 0px 8px;">
<div style="position: relative; margin-right: 15px;">
<form class="example-form">
@@ -8,9 +8,9 @@
<div class="row">
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
<mat-form-field color="accent" class="example-full-width">
<input style="padding-right: 25px;" matInput (keyup.enter)="downloadClicked()" (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput>
<textarea class="url-input" cdkTextareaAutosize cdkAutosizeMinRows="1" wrap="off" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput></textarea>
</mat-form-field>
<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
<!--<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>-->
</div>
<div *ngIf="allowQualitySelect" class="col-7 col-sm-3">
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
@@ -19,12 +19,17 @@
Quality
</ng-container>
</mat-label>
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
{{option.label}}
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged($event)">
<mat-option [value]="''">
Max
</mat-option>
</ng-container>
<ng-container *ngIf="url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats">
<ng-container *ngFor="let option of cachedAvailableFormats[url]['formats'][audioOnly ? 'audio' : 'video']">
<mat-option *ngIf="option.key !== 'best_audio_format'" [value]="option">
{{option.key}}
</mat-option>
</ng-container>
</ng-container>
</mat-select>
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
<mat-spinner [diameter]="25"></mat-spinner>
@@ -60,9 +65,9 @@
Only Audio
</ng-container>
</mat-checkbox>
<mat-checkbox *ngIf="allowMultiDownloadMode" [disabled]="current_download" (change)="multiDownloadModeChanged($event)" [(ngModel)]="multiDownloadMode" style="float: right; margin-top: -12px">
<ng-container i18n="Multi-download Mode checkbox">
Multi-download Mode
<mat-checkbox *ngIf="allowAutoplay" [disabled]="getURLArray(url).length > 1" (change)="autoplayChanged($event)" [(ngModel)]="autoplay" style="float: right; margin-top: -12px">
<ng-container i18n="Autoplay checkbox">
Autoplay
</ng-container>
</mat-checkbox>
@@ -106,8 +111,13 @@
</ng-container>
</mat-checkbox>
<button class="edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Replace args">
Replace args
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" style="margin-bottom: 42px;" class="advanced-input">
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput placeholder="Custom args" i18n-placeholder="Custom args placeholder">
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput (ngModelChange)="argsChanged()" placeholder="Custom args" i18n-placeholder="Custom args placeholder">
<mat-hint>
<ng-container i18n="Custom Args input hint">
No need to include URL, just everything after. Args are delimited using two commas like so: ,,
@@ -122,26 +132,41 @@
</ng-container>
</mat-checkbox>
<mat-form-field style="margin-bottom: 42px;" color="accent" class="advanced-input">
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput placeholder="Custom output" i18n-placeholder="Custom output placeholder">
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput (ngModelChange)="argsChanged()" placeholder="Custom output" i18n-placeholder="Custom output placeholder">
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use authentication checkbox">
Use authentication
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" class="advanced-input">
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Username" i18n-placeholder="YT Username placeholder">
<mat-form-field *ngIf="youtubeAuthEnabled" color="accent" class="advanced-input">
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()" placeholder="Username" i18n-placeholder="YT Username placeholder">
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Password" i18n-placeholder="YT Password placeholder">
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-form-field *ngIf="youtubeAuthEnabled" style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()" placeholder="Password" i18n-placeholder="YT Password placeholder">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Crop video checkbox">
Crop file
</ng-container>
</mat-checkbox>
<mat-form-field *ngIf="cropFile" color="accent" class="advanced-input">
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput placeholder="Crop from (seconds)" i18n-placeholder="Crop from placeholder">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-form-field *ngIf="cropFile" style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput placeholder="Crop to (seconds)" i18n-placeholder="Crop to placeholder">
</mat-form-field>
</div>
</div>
@@ -149,20 +174,8 @@
</mat-expansion-panel>
</form>
</div>
<div *ngIf="multiDownloadMode && downloads.length > 0 && !current_download" style="margin-top: 15px;" class="big demo-basic">
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
<div class="container">
<div *ngFor="let download of downloads; let i = index;" class="row">
<ng-container *ngIf="current_download !== download && download['downloading']">
<app-download-item style="width: 100%" [download]="download" [queueNumber]="i+1" (cancelDownload)="cancelDownload($event)"></app-download-item>
<mat-divider style="position: relative" *ngIf="i !== downloads.length - 1"></mat-divider>
</ng-container>
</div>
</div>
</mat-card>
</div>
<br/>
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
<div class="centered big" id="bar_div" *ngIf="current_download && autoplay">
<div class="margined">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
@@ -177,102 +190,14 @@
</div>
<br/>
</div>
<ng-template #nofile>
</ng-template>
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
<app-downloads style="width: 80%; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
</div>
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<app-recent-videos></app-recent-videos>
<app-recent-videos #recentVideos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
</ng-container>
<!--<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>
<mat-panel-title>
<ng-container i18n="Audio files title">
Audio
</ng-container>
</mat-panel-title>
<mat-panel-description>
<ng-container i18n="Audio files description">
Your audio files are here
</ng-container>
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="mp3s.length > 0;else nomp3s">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="true" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<mat-divider></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;">
<h6 i18n="Playlists title">Playlists</h6>
</div>
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="true" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('audio')" mat-fab><mat-icon>add</mat-icon></button></div>
<div *ngIf="playlists.audio.length === 0">
<ng-container i18n="No video playlists available text">
No playlists available. Create one from your downloading audio files by clicking the blue plus button.
</ng-container>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel (opened)="accordionOpened('video')" (closed)="accordionClosed('video')" (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" class="big">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Video files title">
Video
</ng-container>
</mat-panel-title>
<mat-panel-description>
<ng-container i18n="Video files description">
Your video files are here
</ng-container>
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="mp4s.length > 0;else nomp4s">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="false" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<mat-divider></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;">
<h6 i18n="Playlists title">Playlists</h6>
</div>
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="false" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
</mat-grid-list>
<!-- 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">
No playlists available. Create one from your downloading video files by clicking the blue plus button.
</ng-container>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>-->

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MainComponent } from './main.component';
@@ -6,7 +6,7 @@ describe('MainComponent', () => {
let component: MainComponent;
let fixture: ComponentFixture<MainComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ MainComponent ]
})

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@
.audio-styles {
height: 50px;
background-color: transparent;
width: 100%;
}
@@ -37,10 +36,8 @@
}
.spinner {
width: 50px;
height: 50px;
bottom: 3px;
left: 3px;
bottom: 1px;
left: 2px;
position: absolute;
}
@@ -92,4 +89,10 @@
display: inline-block;
margin-right: 12px;
top: 8px;
}
.skip-ad-button {
position: absolute;
right: 20px;
bottom: 75px;
}

View File

@@ -1,35 +1,69 @@
<div *ngIf="playlist.length > 0 && show_player">
<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; max-height: 75vh" (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 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>
</div>
</div>
</div>
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
<div style="height: 100%" [ngClass]="(currentItem.type === 'audio/mp3') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 100%">
<mat-drawer-container style="height: 100%" class="example-container" autosize>
<div style="height: fit-content" [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
</video>
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" [sponsor_block_cache]="sponsor_block_cache" class="skip-ad-button"></app-skip-ad-button>
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<div class="container">
<div class="row">
<div class="col-2 col-lg-1">
<ng-container *ngIf="db_file">{{db_file['local_view_count'] ? db_file['local_view_count']+1 : 1}}&nbsp;<ng-container i18n="View count label">views</ng-container></ng-container>
</div>
<div style="white-space: pre-line;" class="col-8 col-lg-9">
<ng-container *ngIf="db_file && db_file['description']">
<p>
<app-see-more [text]="db_file['description']"></app-see-more>
</p>
</ng-container>
<ng-container *ngIf="!db_file || !db_file['description']">
<p style="text-align: center;">
No description available.
</p>
</ng-container>
</div>
<div class="col-2">
<ng-container *ngIf="db_playlist">
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
</ng-container>
<ng-container *ngIf="db_file">
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
<button *ngIf="type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
<button (click)="openFileInfoDialog()" *ngIf="db_file" mat-icon-button><mat-icon>info</mat-icon></button>
</ng-container>
<button *ngIf="db_file && db_file.url.includes('twitch.tv/videos/') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
</div>
</div>
</div>
</div>
<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>
</div>
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
<div class="spinner-div">
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
</div>
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container>&nbsp;<mat-icon>update</mat-icon></button>
</div>
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
<div *ngIf="playlist.length > 1">
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
</div>
<div *ngIf="playlist.length === 1">
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv/videos/')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
</ng-container>
</mat-drawer>
<!-- <div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
<div class="spinner-div">
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
</div>
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container>&nbsp;<mat-icon>update</mat-icon></button>
</div> -->
</mat-drawer-container>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More