mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-21 12:10:58 +03:00
Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into api-generator
This commit is contained in:
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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> {{session_downloads.key}}
|
||||
<span *ngIf="session_downloads.key === postsService.session_id"> <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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.edit-role {
|
||||
position: relative;
|
||||
top: -50px;
|
||||
left: 35px;
|
||||
}
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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'];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -47,6 +47,10 @@
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.paginator {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.my-videos-title {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
11
src/app/components/see-more/see-more.component.html
Normal file
11
src/app/components/see-more/see-more.component.html
Normal 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>
|
||||
7
src/app/components/see-more/see-more.component.scss
Normal file
7
src/app/components/see-more/see-more.component.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
25
src/app/components/see-more/see-more.component.spec.ts
Normal file
25
src/app/components/see-more/see-more.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SeeMoreComponent } from './see-more.component';
|
||||
|
||||
describe('SeeMoreComponent', () => {
|
||||
let component: SeeMoreComponent;
|
||||
let fixture: ComponentFixture<SeeMoreComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SeeMoreComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SeeMoreComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
60
src/app/components/see-more/see-more.component.ts
Normal file
60
src/app/components/see-more/see-more.component.ts
Normal 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>")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
115
src/app/components/skip-ad-button/skip-ad-button.component.ts
Normal file
115
src/app/components/skip-ad-button/skip-ad-button.component.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
12
src/app/components/twitch-chat/twitch-chat.component.html
Normal file
12
src/app/components/twitch-chat/twitch-chat.component.html
Normal 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>
|
||||
13
src/app/components/twitch-chat/twitch-chat.component.scss
Normal file
13
src/app/components/twitch-chat/twitch-chat.component.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.downloading-spinner {
|
||||
top: 50%;
|
||||
left: 80px;
|
||||
}
|
||||
25
src/app/components/twitch-chat/twitch-chat.component.spec.ts
Normal file
25
src/app/components/twitch-chat/twitch-chat.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
138
src/app/components/twitch-chat/twitch-chat.component.ts
Normal file
138
src/app/components/twitch-chat/twitch-chat.component.ts
Normal 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;
|
||||
}
|
||||
@@ -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> {{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>
|
||||
|
||||
<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> <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> <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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ]
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user