Fixing coding style

This commit is contained in:
Zoe Roux 2021-03-07 00:42:02 +01:00
parent d36e332bcb
commit 1dbbc0a67a
54 changed files with 590 additions and 589 deletions

View File

@ -1,10 +1,10 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; import { RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
import { ItemsGridComponent } from './components/items-grid/items-grid.component'; import { ItemsGridComponent } from "./components/items-grid/items-grid.component";
import { CustomRouteReuseStrategy } from "./misc/custom-route-reuse-strategy"; import { CustomRouteReuseStrategy } from "./misc/custom-route-reuse-strategy";
import { NotFoundComponent } from './pages/not-found/not-found.component'; import { NotFoundComponent } from "./pages/not-found/not-found.component";
import { PageResolver } from './services/page-resolver.service'; import { PageResolver } from "./services/page-resolver.service";
import { ShowDetailsComponent } from './pages/show-details/show-details.component'; import { ShowDetailsComponent } from "./pages/show-details/show-details.component";
import { AuthGuard } from "./auth/misc/authenticated-guard.service"; import { AuthGuard } from "./auth/misc/authenticated-guard.service";
import { LibraryItem } from "./models/resources/library-item"; import { LibraryItem } from "./models/resources/library-item";
import { import {
@ -92,7 +92,7 @@ const routes: Routes = [
// TODO implement an home page. // TODO implement an home page.
{path: "", pathMatch: 'full', redirectTo: "/browse"}, {path: "", pathMatch: "full", redirectTo: "/browse"},
{path: "**", component: NotFoundComponent} {path: "**", component: NotFoundComponent}
]; ];

View File

@ -15,7 +15,7 @@
<ul class="navbar-nav flex-row flex-nowrap ml-auto"> <ul class="navbar-nav flex-row flex-nowrap ml-auto">
<li class="nav-item icon searchbar"> <li class="nav-item icon searchbar">
<mat-icon matTooltipPosition="below" matTooltip="Search" (click)="openSearch()">search</mat-icon> <mat-icon matTooltipPosition="below" matTooltip="Search" (click)="openSearch()">search</mat-icon>
<input placeholder="Search" id="search" type="search" (input)="onUpdateValue($event)"/> <input placeholder="Search" id="search" type="search" (input)="onUpdateValue($any($event))"/>
</li> </li>
<li class="nav-item" *ngIf="!this.isAuthenticated else accountDrop"> <li class="nav-item" *ngIf="!this.isAuthenticated else accountDrop">

View File

@ -1,4 +1,4 @@
import {Component} from '@angular/core'; import { Component } from "@angular/core";
import { import {
Event, Event,
Router, Router,
@ -13,19 +13,21 @@ import {AccountComponent} from "./auth/account/account.component";
import { AuthService } from "./auth/auth.service"; import { AuthService } from "./auth/auth.service";
import { Library } from "./models/resources/library"; import { Library } from "./models/resources/library";
import { LibraryService } from "./services/api.service"; import { LibraryService } from "./services/api.service";
// noinspection ES6UnusedImports
import * as $ from "jquery"; import * as $ from "jquery";
import ChangeEvent = JQuery.ChangeEvent;
@Component({ @Component({
selector: 'app-root', selector: "app-root",
templateUrl: './app.component.html', templateUrl: "./app.component.html",
styleUrls: ['./app.component.scss'] styleUrls: ["./app.component.scss"]
}) })
export class AppComponent export class AppComponent
{ {
static isMobile: boolean = false;
libraries: Library[]; libraries: Library[];
isLoading: boolean = false; isLoading: boolean = false;
static isMobile: boolean = false;
constructor(private libraryService: LibraryService, constructor(private libraryService: LibraryService,
private router: Router, private router: Router,
@ -61,18 +63,23 @@ export class AppComponent
document.body.classList.add("hoverEnabled"); document.body.classList.add("hoverEnabled");
} }
openSearch() get isAuthenticated(): boolean
{ {
let input: HTMLInputElement = <HTMLInputElement>document.getElementById("search"); return this.authManager.isAuthenticated;
}
openSearch(): void
{
const input: HTMLInputElement = document.getElementById("search") as HTMLInputElement;
input.value = ""; input.value = "";
input.focus(); input.focus();
} }
onUpdateValue(event) onUpdateValue(event: ChangeEvent<HTMLInputElement>): void
{ {
let query: string = event.target.value; const query: string = event.target.value;
if (query != "") if (query !== "")
{ {
event.target.classList.add("searching"); event.target.classList.add("searching");
this.router.navigate(["/search", query], { this.router.navigate(["/search", query], {
@ -86,13 +93,8 @@ export class AppComponent
} }
} }
openAccountDialog() openAccountDialog(): void
{ {
this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.account}); this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.account});
} }
get isAuthenticated(): boolean
{
return this.authManager.isAuthenticated;
}
} }

View File

@ -1,45 +1,45 @@
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from "@angular/common/http";
import { APP_INITIALIZER, NgModule } from "@angular/core"; import { APP_INITIALIZER, NgModule } from "@angular/core";
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from "@angular/material/card";
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from "@angular/material/core";
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from "@angular/material/icon";
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from "@angular/material/menu";
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from "@angular/material/progress-bar";
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from "@angular/material/select";
import { MatSliderModule } from '@angular/material/slider'; import { MatSliderModule } from "@angular/material/slider";
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from "@angular/material/snack-bar";
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from "@angular/material/tooltip";
import { BrowserModule, HammerModule } from "@angular/platform-browser"; import { BrowserModule, HammerModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from './app.component'; import { AppComponent } from "./app.component";
import { ItemsGridComponent } from './components/items-grid/items-grid.component'; import { ItemsGridComponent } from "./components/items-grid/items-grid.component";
import { CollectionComponent } from './pages/collection/collection.component'; import { CollectionComponent } from "./pages/collection/collection.component";
import { EpisodesListComponent } from './components/episodes-list/episodes-list.component'; import { EpisodesListComponent } from "./components/episodes-list/episodes-list.component";
import { NotFoundComponent } from './pages/not-found/not-found.component'; import { NotFoundComponent } from "./pages/not-found/not-found.component";
import { PeopleListComponent } from './components/people-list/people-list.component'; import { PeopleListComponent } from "./components/people-list/people-list.component";
import { import {
BufferToWidthPipe, BufferToWidthPipe,
FormatTimePipe, FormatTimePipe,
PlayerComponent, SupportedButtonPipe, PlayerComponent, SupportedButtonPipe,
VolumeToButtonPipe VolumeToButtonPipe
} from "./pages/player/player.component"; } from "./pages/player/player.component";
import { SearchComponent } from './pages/search/search.component'; import { SearchComponent } from "./pages/search/search.component";
import { ShowDetailsComponent } from './pages/show-details/show-details.component'; import { ShowDetailsComponent } from "./pages/show-details/show-details.component";
import { FormsModule , ReactiveFormsModule } from "@angular/forms"; import { FormsModule , ReactiveFormsModule } from "@angular/forms";
import { MatInputModule } from "@angular/material/input"; import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field"; import { MatFormFieldModule } from "@angular/material/form-field";
import { MatTabsModule } from "@angular/material/tabs"; import { MatTabsModule } from "@angular/material/tabs";
import { PasswordValidator } from "./misc/password-validator"; import { PasswordValidator } from "./misc/password-validator";
import { MatCheckboxModule } from "@angular/material/checkbox"; import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from "@angular/material/dialog";
import { FallbackDirective } from "./misc/fallback.directive"; import { FallbackDirective } from "./misc/fallback.directive";
import { AuthModule } from "./auth/auth.module"; import { AuthModule } from "./auth/auth.module";
import { AuthRoutingModule } from "./auth/auth-routing.module"; import { AuthRoutingModule } from "./auth/auth-routing.module";
import { TrailerDialogComponent } from './pages/trailer-dialog/trailer-dialog.component'; import { TrailerDialogComponent } from "./pages/trailer-dialog/trailer-dialog.component";
import { ItemsListComponent } from "./components/items-list/items-list.component"; import { ItemsListComponent } from "./components/items-list/items-list.component";
import { MetadataEditComponent } from './pages/metadata-edit/metadata-edit.component'; import { MetadataEditComponent } from "./pages/metadata-edit/metadata-edit.component";
import { MatChipsModule } from "@angular/material/chips"; import { MatChipsModule } from "@angular/material/chips";
import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatAutocompleteModule } from "@angular/material/autocomplete";
import { MatExpansionModule } from "@angular/material/expansion"; import { MatExpansionModule } from "@angular/material/expansion";
@ -47,7 +47,7 @@ import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { ShowGridComponent } from "./components/show-grid/show-grid.component"; import { ShowGridComponent } from "./components/show-grid/show-grid.component";
import { MatBadgeModule } from "@angular/material/badge"; import { MatBadgeModule } from "@angular/material/badge";
import { StartupService } from "./services/startup.service"; import { StartupService } from "./services/startup.service";
import { LongPressDirective } from './misc/long-press.directive'; import { LongPressDirective } from "./misc/long-press.directive";
@NgModule({ @NgModule({
@ -105,6 +105,9 @@ import { LongPressDirective } from './misc/long-press.directive';
HammerModule HammerModule
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
exports: [
FallbackDirective
],
providers: [ providers: [
StartupService, StartupService,
{ {

View File

@ -1,13 +1,13 @@
import {Component, ElementRef, Inject, ViewChild} from '@angular/core'; import { Component, ElementRef, Inject, ViewChild } from "@angular/core";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Account } from "../../models/account"; import { Account } from "../../models/account";
@Component({ @Component({
selector: 'app-account', selector: "app-account",
templateUrl: './account.component.html', templateUrl: "./account.component.html",
styleUrls: ['./account.component.scss'] styleUrls: ["./account.component.scss"]
}) })
export class AccountComponent export class AccountComponent
{ {
@ -18,9 +18,9 @@ export class AccountComponent
@Inject(MAT_DIALOG_DATA) public account: Account, @Inject(MAT_DIALOG_DATA) public account: Account,
private http: HttpClient) {} private http: HttpClient) {}
finish() finish(): void
{ {
let data = new FormData(); const data: FormData = new FormData();
data.append("email", this.account.email); data.append("email", this.account.email);
data.append("username", this.account.username); data.append("username", this.account.username);
data.append("picture", this.selectedPicture); data.append("picture", this.selectedPicture);
@ -31,15 +31,15 @@ export class AccountComponent
}); });
} }
cancel() cancel(): void
{ {
this.dialogRef.close(); this.dialogRef.close();
} }
onPictureSelected(event: any) onPictureSelected(event: any): void
{ {
this.selectedPicture = event.target.files[0]; this.selectedPicture = event.target.files[0];
const reader = new FileReader(); const reader: FileReader = new FileReader();
reader.onloadend = () => reader.onloadend = () =>
{ {
this.accountImg.nativeElement.src = reader.result; this.accountImg.nativeElement.src = reader.result;

View File

@ -1,4 +1,4 @@
import {NgModule} from '@angular/core'; import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { UnauthorizedComponent } from "./unauthorized/unauthorized.component"; import { UnauthorizedComponent } from "./unauthorized/unauthorized.component";
import { LogoutComponent } from "./logout/logout.component"; import { LogoutComponent } from "./logout/logout.component";

View File

@ -25,7 +25,7 @@ import { AuthGuard } from "./misc/authenticated-guard.service";
import { AuthorizerInterceptor } from "./misc/authorizer-interceptor.service"; import { AuthorizerInterceptor } from "./misc/authorizer-interceptor.service";
import { UnauthorizedComponent } from "./unauthorized/unauthorized.component"; import { UnauthorizedComponent } from "./unauthorized/unauthorized.component";
export function loadConfig(oidcConfigService: OidcConfigService) export function loadConfig(oidcConfigService: OidcConfigService): () => Promise<any>
{ {
return () => oidcConfigService.withConfig({ return () => oidcConfigService.withConfig({
stsServer: window.location.origin, stsServer: window.location.origin,

View File

@ -1,18 +1,16 @@
import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core";
import { Injectable } from '@angular/core';
import { OidcSecurityService } from "angular-auth-oidc-client"; import { OidcSecurityService } from "angular-auth-oidc-client";
import { Account } from "../models/account"; import { Account } from "../models/account";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class AuthService export class AuthService
{ {
isAuthenticated: boolean = false; isAuthenticated: boolean = false;
account: Account = null; account: Account = null;
constructor(private oidcSecurityService: OidcSecurityService, constructor(private oidcSecurityService: OidcSecurityService)
private http: HttpClient)
{ {
this.oidcSecurityService.checkAuth() this.oidcSecurityService.checkAuth()
.subscribe((auth: boolean) => this.isAuthenticated = auth); .subscribe((auth: boolean) => this.isAuthenticated = auth);
@ -24,17 +22,17 @@ export class AuthService
email: x.email, email: x.email,
username: x.username, username: x.username,
picture: x.picture, picture: x.picture,
permissions: x.permissions.split(',') permissions: x.permissions.split(",")
}; };
}); });
} }
login() login(): void
{ {
this.oidcSecurityService.authorize(); this.oidcSecurityService.authorize();
} }
logout() logout(): void
{ {
// this.http.get("api/account/logout").subscribe(() => // this.http.get("api/account/logout").subscribe(() =>
// { // {

View File

@ -1,9 +1,9 @@
import {Injector, Pipe, PipeTransform} from '@angular/core'; import { Injector, Pipe, PipeTransform } from "@angular/core";
import { HttpClient, HttpHeaders } from "@angular/common/http"; import { HttpClient, HttpHeaders } from "@angular/common/http";
import { OidcSecurityService } from "angular-auth-oidc-client"; import { OidcSecurityService } from "angular-auth-oidc-client";
@Pipe({ @Pipe({
name: 'auth' name: "auth"
}) })
export class AuthPipe implements PipeTransform export class AuthPipe implements PipeTransform
{ {
@ -15,12 +15,12 @@ export class AuthPipe implements PipeTransform
{ {
if (this.oidcSecurity === undefined) if (this.oidcSecurity === undefined)
this.oidcSecurity = this.injector.get(OidcSecurityService); this.oidcSecurity = this.injector.get(OidcSecurityService);
let token = this.oidcSecurity.getToken(); const token: string = this.oidcSecurity.getToken();
if (!token) if (!token)
return uri; return uri;
const headers = new HttpHeaders({"Authorization": "Bearer " + token}); const headers: HttpHeaders = new HttpHeaders({Authorization: "Bearer " + token});
const img = await this.http.get(uri, {headers, responseType: 'blob'}).toPromise(); const img: Blob = await this.http.get(uri, {headers, responseType: "blob"}).toPromise();
const reader = new FileReader(); const reader: FileReader = new FileReader();
return new Promise((resolve) => { return new Promise((resolve) => {
reader.onloadend = () => resolve(reader.result as string); reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(img); reader.readAsDataURL(img);

View File

@ -1,14 +1,6 @@
import {Injectable} from '@angular/core'; import { Injectable } from "@angular/core";
import { import { CanActivate, CanLoad, Router } from "@angular/router";
CanActivate, import { Observable } from "rxjs";
CanLoad,
Route,
UrlSegment,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import {Observable} from 'rxjs';
import { AuthService } from "../auth.service"; import { AuthService } from "../auth.service";
@Injectable({providedIn: "root"}) @Injectable({providedIn: "root"})
@ -18,14 +10,14 @@ export class AuthGuard
public static defaultPermissions: string[]; public static defaultPermissions: string[];
public static permissionsObservable: Observable<string[]>; public static permissionsObservable: Observable<string[]>;
static forPermissions(...permissions: string[]) static forPermissions(...permissions: string[]): any
{ {
@Injectable() @Injectable()
class AuthenticatedGuard implements CanActivate, CanLoad class AuthenticatedGuard implements CanActivate, CanLoad
{ {
constructor(private router: Router, private authManager: AuthService) {} constructor(private router: Router, private authManager: AuthService) {}
async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> async canActivate(): Promise<boolean>
{ {
if (!await this.checkPermissions()) if (!await this.checkPermissions())
{ {
@ -35,7 +27,7 @@ export class AuthGuard
return true; return true;
} }
async canLoad(route: Route, segments: UrlSegment[]): Promise<boolean> async canLoad(): Promise<boolean>
{ {
if (!await this.checkPermissions()) if (!await this.checkPermissions())
{ {
@ -50,7 +42,7 @@ export class AuthGuard
if (this.authManager.isAuthenticated) if (this.authManager.isAuthenticated)
{ {
const perms: string[] = this.authManager.account.permissions; const perms: string[] = this.authManager.account.permissions;
for (let perm of permissions) { for (const perm of permissions) {
if (!perms.includes(perm)) if (!perms.includes(perm))
return false; return false;
} }
@ -58,10 +50,10 @@ export class AuthGuard
} }
else else
{ {
if (AuthGuard.defaultPermissions == undefined) if (!AuthGuard.defaultPermissions)
await AuthGuard.permissionsObservable.toPromise() await AuthGuard.permissionsObservable.toPromise();
for (let perm of permissions) for (const perm of permissions)
if (!AuthGuard.defaultPermissions.includes(perm)) if (!AuthGuard.defaultPermissions.includes(perm))
return false; return false;
return true; return true;

View File

@ -1,11 +1,11 @@
import {Injectable, Injector} from '@angular/core'; import { Injectable, Injector } from "@angular/core";
import { import {
HttpRequest, HttpRequest,
HttpHandler, HttpHandler,
HttpEvent, HttpEvent,
HttpInterceptor HttpInterceptor
} from '@angular/common/http'; } from "@angular/common/http";
import { Observable } from 'rxjs'; import { Observable } from "rxjs";
import { OidcSecurityService } from "angular-auth-oidc-client"; import { OidcSecurityService } from "angular-auth-oidc-client";
@Injectable() @Injectable()
@ -20,7 +20,7 @@ export class AuthorizerInterceptor implements HttpInterceptor
{ {
if (this.oidcSecurity === undefined) if (this.oidcSecurity === undefined)
this.oidcSecurity = this.injector.get(OidcSecurityService); this.oidcSecurity = this.injector.get(OidcSecurityService);
let token = this.oidcSecurity.getToken(); const token: string = this.oidcSecurity.getToken();
if (token) if (token)
request = request.clone({setHeaders: {Authorization: "Bearer " + token}}); request = request.clone({setHeaders: {Authorization: "Bearer " + token}});
return next.handle(request); return next.handle(request);

View File

@ -1,10 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component } from "@angular/core";
import { AuthService } from "../auth.service"; import { AuthService } from "../auth.service";
@Component({ @Component({
selector: 'app-unauthorized', selector: "app-unauthorized",
templateUrl: './unauthorized.component.html', templateUrl: "./unauthorized.component.html",
styleUrls: ['./unauthorized.component.scss'] styleUrls: ["./unauthorized.component.scss"]
}) })
export class UnauthorizedComponent export class UnauthorizedComponent
{ {

View File

@ -30,7 +30,7 @@ export class EpisodesListComponent extends HorizontalScroller
openMenu(index: number): void openMenu(index: number): void
{ {
const menu = this.menus.find((x, i) => i === index); const menu: MatMenuTrigger = this.menus.find((x, i) => i === index);
menu.focus(); menu.focus();
menu.openMenu(); menu.openMenu();
} }

View File

@ -1,7 +1,7 @@
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { ActivatedRoute, ActivatedRouteSnapshot, Params, Router } from "@angular/router"; import { ActivatedRoute, ActivatedRouteSnapshot, Params, Router } from "@angular/router";
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
import { Genre } from "../../models/resources/genre"; import { Genre } from "../../models/resources/genre";
import { LibraryItem } from "../../models/resources/library-item"; import { LibraryItem } from "../../models/resources/library-item";
import { Page } from "../../models/page"; import { Page } from "../../models/page";
@ -14,36 +14,16 @@ import { Studio } from "../../models/resources/studio";
import { ItemsUtils } from "../../misc/items-utils"; import { ItemsUtils } from "../../misc/items-utils";
import { PeopleService, StudioService } from "../../services/api.service"; import { PeopleService, StudioService } from "../../services/api.service";
import { PreLoaderService } from "../../services/pre-loader.service"; import { PreLoaderService } from "../../services/pre-loader.service";
import { Observable } from "rxjs" import { Observable } from "rxjs";
import { catchError, filter, map, mergeAll } from "rxjs/operators"; import { catchError, filter, map, mergeAll } from "rxjs/operators";
@Component({ @Component({
selector: 'app-items-grid', selector: "app-items-grid",
templateUrl: './items-grid.component.html', templateUrl: "./items-grid.component.html",
styleUrls: ['./items-grid.component.scss'] styleUrls: ["./items-grid.component.scss"]
}) })
export class ItemsGridComponent implements OnInit export class ItemsGridComponent implements OnInit
{ {
@Input() page: Page<LibraryItem | Show | ShowRole | Collection>;
@Input() sortEnabled: boolean = true;
complexFiltersEnabled: boolean;
sortType: string = "title";
sortKeys: string[] = ["title", "start year", "end year"]
sortUp: boolean = true;
public static readonly showOnlyFilters: string[] = ["genres", "studio", "people"]
public static readonly filters: string[] = [].concat(...ItemsGridComponent.showOnlyFilters)
filters: {genres: Genre[], studio: Studio, people: People[]} = {genres: [], studio: null, people: []};
genres: Genre[] = [];
studioForm: FormControl = new FormControl();
filteredStudios: Observable<Studio[]>;
peopleForm: FormControl = new FormControl();
filteredPeople: Observable<People[]>;
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
@ -70,57 +50,103 @@ export class ItemsGridComponent implements OnInit
}); });
} }
updateGenresFilterFromQuery(query: Params) public static readonly showOnlyFilters: string[] = ["genres", "studio", "people"];
public static readonly filters: string[] = [].concat(...ItemsGridComponent.showOnlyFilters);
@Input() page: Page<LibraryItem | Show | ShowRole | Collection>;
@Input() sortEnabled: boolean = true;
complexFiltersEnabled: boolean;
sortType: string = "title";
sortKeys: string[] = ["title", "start year", "end year"];
sortUp: boolean = true;
filters: {genres: Genre[], studio: Studio, people: People[]} = {genres: [], studio: null, people: []};
genres: Genre[] = [];
studioForm: FormControl = new FormControl();
filteredStudios: Observable<Studio[]>;
peopleForm: FormControl = new FormControl();
filteredPeople: Observable<People[]>;
/*
* /browse -> /api/items | /api/shows
* /browse/:library -> /api/library/:slug/items | /api/library/:slug/shows
* /genre/:slug -> /api/shows
* /studio/:slug -> /api/shows
*
* /collection/:slug -> /api/collection/:slug/shows |> /api/collections/:slug/shows
* /people/:slug -> /api/people/:slug/roles |> /api/people/:slug/roles
*/
static routeMapper(route: ActivatedRouteSnapshot, endpoint: string, query: [string, string][]): string
{
const queryParams: [string, string][] = Object.entries(route.queryParams)
.filter(x => ItemsGridComponent.filters.includes(x[0]) || x[0] === "sortBy");
if (query)
queryParams.push(...query);
if (queryParams.some(x => ItemsGridComponent.showOnlyFilters.includes(x[0])))
endpoint = endpoint.replace(/items?$/, "show");
const params: string = queryParams.length > 0
? "?" + queryParams.map(x => `${x[0]}=${x[1]}`).join("&")
: "";
return `api/${endpoint}${params}`;
}
updateGenresFilterFromQuery(query: Params): void
{ {
let selectedGenres: string[] = []; let selectedGenres: string[] = [];
if (query.genres?.startsWith("ctn:")) if (query.genres?.startsWith("ctn:"))
selectedGenres = query.genres.substr(4).split(','); selectedGenres = query.genres.substr(4).split(",");
else if (query.genres != null) else if (query.genres != null)
selectedGenres = query.genres.split(','); selectedGenres = query.genres.split(",");
if (this.router.url.startsWith("/genre")) if (this.router.url.startsWith("/genre"))
selectedGenres.push(this.route.snapshot.params.slug); selectedGenres.push(this.route.snapshot.params.slug);
this.filters.genres = this.genres.filter(x => selectedGenres.includes(x.slug)); this.filters.genres = this.genres.filter(x => selectedGenres.includes(x.slug));
} }
updateStudioFilterFromQuery(query: Params) updateStudioFilterFromQuery(query: Params): void
{ {
const slug: string = this.router.url.startsWith("/studio") ? this.route.snapshot.params.slug : query.studio; const slug: string = this.router.url.startsWith("/studio") ? this.route.snapshot.params.slug : query.studio;
if (slug && this.filters.studio?.slug != slug) if (slug && this.filters.studio?.slug !== slug)
{ {
this.filters.studio = {id: 0, slug: slug, name: slug}; this.filters.studio = {id: 0, slug, name: slug};
this.studioApi.get(slug).subscribe(x => this.filters.studio = x); this.studioApi.get(slug).subscribe(x => this.filters.studio = x);
} }
else if (!slug) else if (!slug)
this.filters.studio = null; this.filters.studio = null;
} }
updatePeopleFilterFromQuery(query: Params) updatePeopleFilterFromQuery(query: Params): void
{ {
let slugs: string[] = []; let slugs: string[] = [];
if (query.people != null) if (query.people != null)
{ {
if (query.people.startsWith("ctn:")) if (query.people.startsWith("ctn:"))
slugs = query.people.substr(4).split(','); slugs = query.people.substr(4).split(",");
else else
slugs = query.people.split(','); slugs = query.people.split(",");
} }
else if (this.route.snapshot.params.slug && this.router.url.startsWith("/people")) else if (this.route.snapshot.params.slug && this.router.url.startsWith("/people"))
slugs = [this.route.snapshot.params.slug]; slugs = [this.route.snapshot.params.slug];
this.filters.people = slugs.map(x => ({slug: x, name: x} as People)); this.filters.people = slugs.map(x => ({slug: x, name: x} as People));
for (let slug of slugs) for (const slug of slugs)
{ {
this.peopleApi.get(slug).subscribe(x => this.peopleApi.get(slug).subscribe(x =>
{ {
let i: number = this.filters.people.findIndex(x => x.slug == slug); const i: number = this.filters.people.findIndex(y => y.slug === slug);
this.filters.people[i] = x this.filters.people[i] = x;
}); });
} }
} }
ngOnInit() ngOnInit(): void
{ {
this.filteredStudios = this.studioForm.valueChanges this.filteredStudios = this.studioForm.valueChanges
.pipe( .pipe(
@ -149,38 +175,12 @@ export class ItemsGridComponent implements OnInit
); );
} }
shouldDisplayNoneStudio() shouldDisplayNoneStudio(): boolean
{ {
return this.studioForm.value == '' || typeof this.studioForm.value != "string"; return this.studioForm.value === "" || typeof this.studioForm.value !== "string";
} }
/* getFilterCount(): number
* /browse -> /api/items | /api/shows
* /browse/:library -> /api/library/:slug/items | /api/library/:slug/shows
* /genre/:slug -> /api/shows
* /studio/:slug -> /api/shows
*
* /collection/:slug -> /api/collection/:slug/shows |> /api/collections/:slug/shows
* /people/:slug -> /api/people/:slug/roles |> /api/people/:slug/roles
*/
static routeMapper(route: ActivatedRouteSnapshot, endpoint: string, query: [string, string][]): string
{
let queryParams: [string, string][] = Object.entries(route.queryParams)
.filter(x => ItemsGridComponent.filters.includes(x[0]) || x[0] == "sortBy");
if (query)
queryParams.push(...query)
if (queryParams.some(x => ItemsGridComponent.showOnlyFilters.includes(x[0])))
endpoint = endpoint.replace(/items?$/, "show");
let params: string = queryParams.length > 0
? '?' + queryParams.map(x => `${x[0]}=${x[1]}`).join('&')
: "";
return `api/${endpoint}${params}`
}
getFilterCount()
{ {
let count: number = this.filters.genres.length + this.filters.people.length; let count: number = this.filters.genres.length + this.filters.people.length;
if (this.filters.studio != null) if (this.filters.studio != null)
@ -188,39 +188,39 @@ export class ItemsGridComponent implements OnInit
return count; return count;
} }
addFilter(category: string, filter: IResource, isArray: boolean = true, toggle: boolean = false) addFilter(category: string, resource: IResource, isArray: boolean = true, toggle: boolean = false): void
{ {
if (isArray) if (isArray)
{ {
if (this.filters[category].includes(filter) || this.filters[category].some(x => x.slug == filter.slug)) if (this.filters[category].includes(resource) || this.filters[category].some(x => x.slug === resource.slug))
this.filters[category].splice(this.filters[category].indexOf(filter), 1); this.filters[category].splice(this.filters[category].indexOf(resource), 1);
else else
this.filters[category].push(filter); this.filters[category].push(resource);
} }
else else
{ {
if (filter && (this.filters[category] == filter || this.filters[category]?.slug == filter.slug)) if (resource && (this.filters[category] === resource || this.filters[category]?.slug === resource.slug))
{ {
if (!toggle) if (!toggle)
return; return;
this.filters[category] = null; this.filters[category] = null;
} }
else else
this.filters[category] = filter; this.filters[category] = resource;
} }
let param: string = null; let param: string = null;
if (isArray && this.filters[category].length > 0) if (isArray && this.filters[category].length > 0)
param = `${this.filters[category].map(x => x.slug).join(',')}`; param = `${this.filters[category].map(x => x.slug).join(",")}`;
else if (!isArray && this.filters[category] != null) else if (!isArray && this.filters[category] != null)
param = filter.slug; param = resource.slug;
if (/\/browse($|\?)/.test(this.router.url) if (/\/browse($|\?)/.test(this.router.url)
|| this.router.url.startsWith("/genre") || this.router.url.startsWith("/genre")
|| this.router.url.startsWith("/studio") || this.router.url.startsWith("/studio")
|| this.router.url.startsWith("/people")) || this.router.url.startsWith("/people"))
{ {
if (this.filters.genres.length == 1 && this.getFilterCount() == 1) if (this.filters.genres.length === 1 && this.getFilterCount() === 1)
{ {
this.router.navigate(["genre", this.filters.genres[0].slug], { this.router.navigate(["genre", this.filters.genres[0].slug], {
replaceUrl: true, replaceUrl: true,
@ -228,7 +228,7 @@ export class ItemsGridComponent implements OnInit
}); });
return; return;
} }
if (this.filters.studio != null && this.getFilterCount() == 1) if (this.filters.studio != null && this.getFilterCount() === 1)
{ {
this.router.navigate(["studio", this.filters.studio.slug], { this.router.navigate(["studio", this.filters.studio.slug], {
replaceUrl: true, replaceUrl: true,
@ -236,7 +236,7 @@ export class ItemsGridComponent implements OnInit
}); });
return; return;
} }
if (this.filters.people.length == 1 && this.getFilterCount() == 1) if (this.filters.people.length === 1 && this.getFilterCount() === 1)
{ {
this.router.navigate(["people", this.filters.people[0].slug], { this.router.navigate(["people", this.filters.people[0].slug], {
replaceUrl: true, replaceUrl: true,
@ -244,14 +244,14 @@ export class ItemsGridComponent implements OnInit
}); });
return; return;
} }
if (this.getFilterCount() == 0 || this.router.url != "/browse") if (this.getFilterCount() === 0 || this.router.url !== "/browse")
{ {
let params = {[category]: param} const params: {[key: string]: string} = {[category]: param};
if (this.router.url.startsWith("/studio") && category != "studio") if (this.router.url.startsWith("/studio") && category !== "studio")
params.studio = this.route.snapshot.params.slug; params.studio = this.route.snapshot.params.slug;
if (this.router.url.startsWith("/genre") && category != "genres") if (this.router.url.startsWith("/genre") && category !== "genres")
params.genres = `${this.route.snapshot.params.slug}`; params.genres = `${this.route.snapshot.params.slug}`;
if (this.router.url.startsWith("/people") && category != "people") if (this.router.url.startsWith("/people") && category !== "people")
params.people = `${this.route.snapshot.params.slug}`; params.people = `${this.route.snapshot.params.slug}`;
this.router.navigate(["/browse"], { this.router.navigate(["/browse"], {
@ -270,32 +270,32 @@ export class ItemsGridComponent implements OnInit
}); });
} }
nameGetter(obj: Studio) nameGetter(obj: Studio): string
{ {
return obj?.name ?? "None"; return obj?.name ?? "None";
} }
getThumb(slug: string) getThumb(slug: string): SafeStyle
{ {
return this.sanitizer.bypassSecurityTrustStyle("url(/poster/" + slug + ")"); return this.sanitizer.bypassSecurityTrustStyle("url(/poster/" + slug + ")");
} }
getDate(item: LibraryItem | Show | ShowRole | Collection) getDate(item: LibraryItem | Show | ShowRole | Collection): string
{ {
return ItemsUtils.getDate(item); return ItemsUtils.getDate(item);
} }
getLink(item: LibraryItem | Show | ShowRole | Collection) getLink(item: LibraryItem | Show | ShowRole | Collection): string
{ {
return ItemsUtils.getLink(item); return ItemsUtils.getLink(item);
} }
sort(type: string, order: boolean) sort(type: string, order: boolean): void
{ {
this.sortType = type; this.sortType = type;
this.sortUp = order; this.sortUp = order;
let param: string = `${this.sortType.replace(/\s/g, "")}:${this.sortUp ? "asc" : "desc"}`; const param: string = `${this.sortType.replace(/\s/g, "")}:${this.sortUp ? "asc" : "desc"}`;
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { sortBy: param }, queryParams: { sortBy: param },

View File

@ -1,6 +1,6 @@
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { Collection } from "../../models/resources/collection"; import { Collection } from "../../models/resources/collection";
import {DomSanitizer} from "@angular/platform-browser"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { HorizontalScroller } from "../../misc/horizontal-scroller"; import { HorizontalScroller } from "../../misc/horizontal-scroller";
import { Page } from "../../models/page"; import { Page } from "../../models/page";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
@ -9,9 +9,9 @@ import {LibraryItem} from "../../models/resources/library-item";
import { ItemsUtils } from "../../misc/items-utils"; import { ItemsUtils } from "../../misc/items-utils";
@Component({ @Component({
selector: 'app-items-list', selector: "app-items-list",
templateUrl: './items-list.component.html', templateUrl: "./items-list.component.html",
styleUrls: ['./items-list.component.scss'] styleUrls: ["./items-list.component.scss"]
}) })
export class ItemsListComponent extends HorizontalScroller export class ItemsListComponent extends HorizontalScroller
{ {
@ -22,17 +22,17 @@ export class ItemsListComponent extends HorizontalScroller
super(); super();
} }
getThumb(slug: string) getThumb(slug: string): SafeUrl
{ {
return this.sanitizer.bypassSecurityTrustStyle("url(/poster/" + slug + ")"); return this.sanitizer.bypassSecurityTrustStyle("url(/poster/" + slug + ")");
} }
getDate(item: LibraryItem | Show | ShowRole | Collection) getDate(item: LibraryItem | Show | ShowRole | Collection): string
{ {
return ItemsUtils.getDate(item); return ItemsUtils.getDate(item);
} }
getLink(item: LibraryItem | Show | ShowRole | Collection) getLink(item: LibraryItem | Show | ShowRole | Collection): string
{ {
return ItemsUtils.getLink(item); return ItemsUtils.getLink(item);
} }

View File

@ -1,15 +1,14 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { Component, Input } from "@angular/core";
import { MatButton } from "@angular/material/button"; import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
import { DomSanitizer } from "@angular/platform-browser";
import { People } from "../../models/resources/people"; import { People } from "../../models/resources/people";
import { HorizontalScroller } from "../../misc/horizontal-scroller"; import { HorizontalScroller } from "../../misc/horizontal-scroller";
import { Page } from "../../models/page"; import { Page } from "../../models/page";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
@Component({ @Component({
selector: 'app-people-list', selector: "app-people-list",
templateUrl: './people-list.component.html', templateUrl: "./people-list.component.html",
styleUrls: ['./people-list.component.scss'] styleUrls: ["./people-list.component.scss"]
}) })
export class PeopleListComponent extends HorizontalScroller export class PeopleListComponent extends HorizontalScroller
{ {
@ -20,7 +19,7 @@ export class PeopleListComponent extends HorizontalScroller
super(); super();
} }
getPeopleIcon(slug: string) getPeopleIcon(slug: string): SafeStyle
{ {
return this.sanitizer.bypassSecurityTrustStyle("url(/peopleimg/" + slug + ")"); return this.sanitizer.bypassSecurityTrustStyle("url(/peopleimg/" + slug + ")");
} }

View File

@ -1,27 +1,27 @@
import {Component, EventEmitter, Input, Output} from '@angular/core'; import { Component, EventEmitter, Input, Output } from "@angular/core";
import {DomSanitizer} from "@angular/platform-browser"; import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
import { Show } from "../../models/resources/show"; import { Show } from "../../models/resources/show";
import { Page } from "../../models/page"; import { Page } from "../../models/page";
@Component({ @Component({
selector: 'app-shows-grid', selector: "app-shows-grid",
templateUrl: './show-grid.component.html', templateUrl: "./show-grid.component.html",
styleUrls: ['./show-grid.component.scss'] styleUrls: ["./show-grid.component.scss"]
}) })
export class ShowGridComponent export class ShowGridComponent
{ {
@Input() shows: Page<Show> @Input() shows: Page<Show>;
@Input() externalShows: boolean = false; @Input() externalShows: boolean = false;
@Output() clickCallback: EventEmitter<Show> = new EventEmitter(); @Output() clickCallback: EventEmitter<Show> = new EventEmitter();
constructor(private sanitizer: DomSanitizer) { } constructor(private sanitizer: DomSanitizer) { }
getThumb(show: Show) getThumb(show: Show): SafeStyle
{ {
return this.sanitizer.bypassSecurityTrustStyle(`url(${show.poster})`); return this.sanitizer.bypassSecurityTrustStyle(`url(${show.poster})`);
} }
getLink(show: Show) getLink(show: Show): string
{ {
if (this.externalShows) if (this.externalShows)
return null; return null;

View File

@ -4,30 +4,30 @@ export class CustomRouteReuseStrategy extends RouteReuseStrategy
{ {
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean
{ {
if (curr.routeConfig?.path == "browse" if (curr.routeConfig?.path === "browse"
|| curr.routeConfig?.path == "genre/:slug" || curr.routeConfig?.path === "genre/:slug"
|| curr.routeConfig?.path == "studio/:slug") || curr.routeConfig?.path === "studio/:slug")
{ {
return future.routeConfig.path == "browse" return future.routeConfig.path === "browse"
|| future.routeConfig.path == "genre/:slug" || future.routeConfig.path === "genre/:slug"
|| future.routeConfig.path == "studio/:slug"; || future.routeConfig.path === "studio/:slug";
} }
return future.routeConfig === curr.routeConfig; return future.routeConfig === curr.routeConfig;
} }
shouldAttach(route: ActivatedRouteSnapshot): boolean shouldAttach(): boolean
{ {
return false; return false;
} }
shouldDetach(route: ActivatedRouteSnapshot): boolean shouldDetach(): boolean
{ {
return false; return false;
} }
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {} store(): void {}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null retrieve(): DetachedRouteHandle | null
{ {
return null; return null;
} }

View File

@ -1,10 +1,7 @@
import { Component, ElementRef, ViewChild } from "@angular/core"; import { ElementRef, ViewChild } from "@angular/core";
import { MatButton } from "@angular/material/button"; import { MatButton } from "@angular/material/button";
// noinspection AngularMissingOrInvalidDeclarationInModule
@Component({
template: ""
})
export class HorizontalScroller export class HorizontalScroller
{ {
@ViewChild("scrollView", { static: true }) private scrollView: ElementRef; @ViewChild("scrollView", { static: true }) private scrollView: ElementRef;
@ -12,35 +9,37 @@ export class HorizontalScroller
@ViewChild("rightBtn", { static: false }) private rightBtn: MatButton; @ViewChild("rightBtn", { static: false }) private rightBtn: MatButton;
@ViewChild("itemsDom", { static: false }) private itemsDom: ElementRef; @ViewChild("itemsDom", { static: false }) private itemsDom: ElementRef;
scrollLeft() scrollLeft(): void
{ {
let scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80); const scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80);
this.scrollView.nativeElement.scrollBy({ top: 0, left: -scroll, behavior: "smooth" }); this.scrollView.nativeElement.scrollBy({ top: 0, left: -scroll, behavior: "smooth" });
} }
scrollRight() scrollRight(): void
{ {
let scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80); const scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80);
this.scrollView.nativeElement.scrollBy({ top: 0, left: scroll, behavior: "smooth" }); this.scrollView.nativeElement.scrollBy({ top: 0, left: scroll, behavior: "smooth" });
} }
roundScroll(offset: number): number roundScroll(offset: number): number
{ {
let itemSize: number = this.itemsDom.nativeElement.scrollWidth; const itemSize: number = this.itemsDom.nativeElement.scrollWidth;
offset = Math.round(offset / itemSize) * itemSize; offset = Math.round(offset / itemSize) * itemSize;
if (offset == 0) if (offset === 0)
offset = itemSize; offset = itemSize;
return offset; return offset;
} }
onScroll() onScroll(): void
{ {
if (this.scrollView.nativeElement.scrollLeft <= 0) const scroll: any = this.scrollView.nativeElement;
if (scroll.scrollLeft <= 0)
this.leftBtn._elementRef.nativeElement.classList.add("d-none"); this.leftBtn._elementRef.nativeElement.classList.add("d-none");
else else
this.leftBtn._elementRef.nativeElement.classList.remove("d-none"); this.leftBtn._elementRef.nativeElement.classList.remove("d-none");
if (this.scrollView.nativeElement.scrollLeft >= this.scrollView.nativeElement.scrollWidth - this.scrollView.nativeElement.clientWidth) if (scroll.scrollLeft >= scroll.scrollWidth - scroll.clientWidth)
this.rightBtn._elementRef.nativeElement.classList.add("d-none"); this.rightBtn._elementRef.nativeElement.classList.add("d-none");
else else
this.rightBtn._elementRef.nativeElement.classList.remove("d-none"); this.rightBtn._elementRef.nativeElement.classList.remove("d-none");

View File

@ -7,7 +7,7 @@ export class ItemsUtils
{ {
static getLink(item: LibraryItem | Show | ShowRole | Collection): string static getLink(item: LibraryItem | Show | ShowRole | Collection): string
{ {
if ("type" in item && item.type == ItemType.Collection) if ("type" in item && item.type === ItemType.Collection)
return "/collection/" + item.slug; return "/collection/" + item.slug;
else else
return "/show/" + item.slug; return "/show/" + item.slug;
@ -21,13 +21,13 @@ export class ItemsUtils
return `as ${item.role} (${item.type})`; return `as ${item.role} (${item.type})`;
return `as ${item.role}`; return `as ${item.role}`;
} }
if ("type" in item && item.type && typeof item.type == "string") if ("type" in item && item.type && typeof item.type === "string")
return item.type; return item.type;
if (!("startYear" in item)) if (!("startYear" in item))
return ""; return "";
if (item.endYear && item.startYear != item.endYear) if (item.endYear && item.startYear !== item.endYear)
return `${item.startYear} - ${item.endYear}` return `${item.startYear} - ${item.endYear}`;
return item.startYear?.toString(); return item.startYear?.toString();
} }
} }

View File

@ -1,8 +1,10 @@
import { Directive, Output, EventEmitter, HostListener, HostBinding, ElementRef } from "@angular/core"; import { Directive, Output, EventEmitter, HostListener, HostBinding, ElementRef } from "@angular/core";
import MouseDownEvent = JQuery.MouseDownEvent; import MouseDownEvent = JQuery.MouseDownEvent;
import TouchStartEvent = JQuery.TouchStartEvent; import TouchStartEvent = JQuery.TouchStartEvent;
import ContextMenuEvent = JQuery.ContextMenuEvent;
import ClickEvent = JQuery.ClickEvent;
function cancelClick(event): void function cancelClick(event: ClickEvent): void
{ {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -19,7 +21,10 @@ export class LongPressDirective
constructor(private ref: ElementRef) {} constructor(private ref: ElementRef) {}
@HostBinding('class.longpress') @HostBinding("style.-webkit-touch-callout")
defaultLongTouchEvent: string = "none";
@HostBinding("class.longpress")
get longPress(): boolean get longPress(): boolean
{ {
return this._timer !== null; return this._timer !== null;
@ -29,10 +34,10 @@ export class LongPressDirective
@HostListener("mousedown", ["$event"]) @HostListener("mousedown", ["$event"])
start(event: MouseDownEvent | TouchStartEvent): void start(event: MouseDownEvent | TouchStartEvent): void
{ {
const startBox = event.target.getBoundingClientRect(); const startBox: DOMRect = event.target.getBoundingClientRect();
this._timer = setTimeout(() => this._timer = setTimeout(() =>
{ {
const endBox = event.target.getBoundingClientRect(); const endBox: DOMRect = event.target.getBoundingClientRect();
if (startBox.top !== endBox.top || startBox.left !== endBox.left) if (startBox.top !== endBox.top || startBox.left !== endBox.left)
return; return;
this.longPressed.emit(); this.longPressed.emit();
@ -63,11 +68,8 @@ export class LongPressDirective
} }
@HostListener("contextmenu", ["$event"]) @HostListener("contextmenu", ["$event"])
context(event): void context(event: ContextMenuEvent): void
{ {
event.preventDefault(); event.preventDefault();
} }
@HostBinding("style.-webkit-touch-callout")
defaultLongTouchEvent: string = "none";
} }

View File

@ -2,7 +2,7 @@ import {AbstractControl, NG_VALIDATORS, Validator} from "@angular/forms";
import { Directive } from "@angular/core"; import { Directive } from "@angular/core";
@Directive({ @Directive({
selector: "[passwordValidator]", selector: "[appPasswordValidator]",
providers: [{provide: NG_VALIDATORS, useExisting: PasswordValidator, multi: true}] providers: [{provide: NG_VALIDATORS, useExisting: PasswordValidator, multi: true}]
}) })
export class PasswordValidator implements Validator export class PasswordValidator implements Validator
@ -12,15 +12,15 @@ export class PasswordValidator implements Validator
if (!control.value) if (!control.value)
return null; return null;
if (!/[a-z]/.test(control.value)) if (!/[a-z]/.test(control.value))
return {"passwordError": {error: "The password must contains a lowercase letter."}}; return {passwordError: {error: "The password must contains a lowercase letter."}};
if (!/[A-Z]/.test(control.value)) if (!/[A-Z]/.test(control.value))
return {"passwordError": {error: "The password must contains an uppercase letter."}}; return {passwordError: {error: "The password must contains an uppercase letter."}};
if (!/[0-9]/.test(control.value)) if (!/[0-9]/.test(control.value))
return {"passwordError": {error: "The password must contains a digit."}}; return {passwordError: {error: "The password must contains a digit."}};
if (!/\W/.test(control.value)) if (!/\W/.test(control.value))
return {"passwordError": {error: "The password must contains a non-alphanumeric character."}}; return {passwordError: {error: "The password must contains a non-alphanumeric character."}};
if (control.value.toString().length < 6) if (control.value.toString().length < 6)
return {"passwordError": {error: "Password must be at least 6 character long."}}; return {passwordError: {error: "Password must be at least 6 character long."}};
return null; return null;
} }
} }

View File

@ -1,4 +1,4 @@
import {Provider} from "./provider" import { Provider } from "./provider";
export interface ExternalID export interface ExternalID
{ {

View File

@ -14,7 +14,7 @@ export class Page<T>
Object.assign(this, init); Object.assign(this, init);
} }
loadNext(client: HttpClient) loadNext(client: HttpClient): void
{ {
if (this.next == null || this._isLoading) if (this.next == null || this._isLoading)
return; return;
@ -30,8 +30,8 @@ export class Page<T>
}); });
} }
changeType(type: string) changeType(type: string): string
{ {
return this.first.replace(/\/\w*($|\?)/, `/${type}$1`) return this.first.replace(/\/\w*($|\?)/, `/${type}$1`);
} }
} }

View File

@ -6,7 +6,7 @@ export interface Collection extends IResource
name: string; name: string;
poster: string; poster: string;
overview: string; overview: string;
startYear: number, startYear: number;
endYear: number, endYear: number;
shows: Show[]; shows: Show[];
} }

View File

@ -8,7 +8,7 @@ export interface Episode extends IResource
title: string; title: string;
thumb: string; thumb: string;
overview: string; overview: string;
releaseDate; releaseDate: string;
runtime: number; runtime: number;
showTitle: string; showTitle: string;
externalIDs: ExternalID[]; externalIDs: ExternalID[];

View File

@ -9,12 +9,12 @@ export enum ItemType
export interface LibraryItem extends IResource export interface LibraryItem extends IResource
{ {
title: string title: string;
overview: string overview: string;
status: string status: string;
trailerUrl: string trailerUrl: string;
startYear: number startYear: number;
endYear: number endYear: number;
poster: string poster: string;
type: ItemType type: ItemType;
} }

View File

@ -8,5 +8,5 @@ export interface Season extends IResource
title: string; title: string;
overview: string; overview: string;
episodes: Episode[]; episodes: Episode[];
externalIDs: ExternalID[] externalIDs: ExternalID[];
} }

View File

@ -9,7 +9,7 @@ export interface WatchItem
title: string; title: string;
slug: string; slug: string;
duration: number; duration: number;
releaseDate; releaseDate: string;
isMovie: boolean; isMovie: boolean;
previousEpisode: Episode; previousEpisode: Episode;

View File

@ -1,7 +1,7 @@
import {Component} from '@angular/core'; import { Component } from "@angular/core";
import { Collection } from "../../models/resources/collection"; import { Collection } from "../../models/resources/collection";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import {DomSanitizer} from "@angular/platform-browser"; import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
import { Show, ShowRole } from "../../models/resources/show"; import { Show, ShowRole } from "../../models/resources/show";
import { Page } from "../../models/page"; import { Page } from "../../models/page";
import { People } from "../../models/resources/people"; import { People } from "../../models/resources/people";
@ -9,9 +9,9 @@ import {LibraryItem} from "../../models/resources/library-item";
import { ItemsUtils } from "../../misc/items-utils"; import { ItemsUtils } from "../../misc/items-utils";
@Component({ @Component({
selector: 'app-collection', selector: "app-collection",
templateUrl: './collection.component.html', templateUrl: "./collection.component.html",
styleUrls: ['./collection.component.scss'] styleUrls: ["./collection.component.scss"]
}) })
export class CollectionComponent export class CollectionComponent
{ {
@ -27,12 +27,12 @@ export class CollectionComponent
}); });
} }
getThumb() getThumb(): SafeStyle
{ {
return this.sanitizer.bypassSecurityTrustStyle("url(" + this.collection.poster + ")"); return this.sanitizer.bypassSecurityTrustStyle("url(" + this.collection.poster + ")");
} }
getDate(item: LibraryItem | Show | ShowRole | Collection | People) getDate(item: LibraryItem | Show | ShowRole | Collection | People): string
{ {
return ItemsUtils.getDate(item); return ItemsUtils.getDate(item);
} }

View File

@ -14,11 +14,12 @@ import { Provider } from "../../models/provider";
import { MatSnackBar } from "@angular/material/snack-bar"; import { MatSnackBar } from "@angular/material/snack-bar";
import { ShowGridComponent } from "../../components/show-grid/show-grid.component"; import { ShowGridComponent } from "../../components/show-grid/show-grid.component";
import { GenreService, ShowService, StudioService } from "../../services/api.service"; import { GenreService, ShowService, StudioService } from "../../services/api.service";
import { ExternalID } from "../../models/external-id";
@Component({ @Component({
selector: 'app-metadata-edit', selector: "app-metadata-edit",
templateUrl: './metadata-edit.component.html', templateUrl: "./metadata-edit.component.html",
styleUrls: ['./metadata-edit.component.scss'] styleUrls: ["./metadata-edit.component.scss"]
}) })
export class MetadataEditComponent implements OnInit export class MetadataEditComponent implements OnInit
{ {
@ -29,8 +30,8 @@ export class MetadataEditComponent implements OnInit
filteredGenres: Observable<Genre[]>; filteredGenres: Observable<Genre[]>;
@ViewChild("identifyGrid") private identifyGrid: ShowGridComponent; @ViewChild("identifyGrid") private identifyGrid: ShowGridComponent;
private identifying: Observable<Show[]>; private _identifying: Observable<Show[]>;
private identifiedShows: [string, Show[]]; private _identifiedShows: [string, Show[]];
public providers: Provider[] = []; public providers: Provider[] = [];
public metadataChanged: boolean = false; public metadataChanged: boolean = false;
@ -51,7 +52,7 @@ export class MetadataEditComponent implements OnInit
this.reIdentify(this.show.title); this.reIdentify(this.show.title);
} }
ngOnInit() ngOnInit(): void
{ {
this.filteredGenres = this.genreForm.valueChanges this.filteredGenres = this.genreForm.valueChanges
.pipe( .pipe(
@ -85,13 +86,14 @@ export class MetadataEditComponent implements OnInit
if (this.metadataChanged) if (this.metadataChanged)
{ {
this.http.post("/api/show/re-identify/" + this.show.slug, this.show.externalIDs).subscribe( this.http.post("/api/show/re-identify/" + this.show.slug, this.show.externalIDs).subscribe(
(show: Show) => () => {},
{
return;
},
() => () =>
{ {
this.snackBar.open("An unknown error occurred.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); this.snackBar.open("An unknown error occurred.", null, {
horizontalPosition: "left",
panelClass: ["snackError"],
duration: 2500
});
} }
); );
this.dialogRef.close(this.show); this.dialogRef.close(this.show);
@ -105,27 +107,27 @@ export class MetadataEditComponent implements OnInit
} }
} }
addAlias(event: MatChipInputEvent) addAlias(event: MatChipInputEvent): void
{ {
const input = event.input; const input: HTMLInputElement = event.input;
const value = event.value; const value: string = event.value;
this.show.aliases.push(value); this.show.aliases.push(value);
if (input) if (input)
input.value = ""; input.value = "";
} }
removeAlias(alias: string) removeAlias(alias: string): void
{ {
const i = this.show.aliases.indexOf(alias); const i: number = this.show.aliases.indexOf(alias);
this.show.aliases.splice(i, 1); this.show.aliases.splice(i, 1);
} }
addGenre(event: MatChipInputEvent) addGenre(event: MatChipInputEvent): void
{ {
const input = event.input; const input: HTMLInputElement = event.input;
const value = event.value; const value: string = event.value;
let genre: Genre = {id: 0, slug: null, name: value}; const genre: Genre = {id: 0, slug: null, name: value};
this.show.genres.push(genre); this.show.genres.push(genre);
if (input) if (input)
@ -134,7 +136,7 @@ export class MetadataEditComponent implements OnInit
removeGenre(genre: Genre): void removeGenre(genre: Genre): void
{ {
const i = this.show.genres.indexOf(genre); const i: number = this.show.genres.indexOf(genre);
this.show.genres.splice(i, 1); this.show.genres.splice(i, 1);
} }
@ -145,41 +147,42 @@ export class MetadataEditComponent implements OnInit
identityShow(name: string): Observable<Show[]> identityShow(name: string): Observable<Show[]>
{ {
if (this.identifiedShows && this.identifiedShows[0] === name) if (this._identifiedShows && this._identifiedShows[0] === name)
return of(this.identifiedShows[1]); return of(this._identifiedShows[1]);
this.identifying = this.http.get<Show[]>("/api/show/identify/" + name + "?isMovie=" + this.show.isMovie).pipe( this._identifying = this.http.get<Show[]>("/api/show/identify/" + name + "?isMovie=" + this.show.isMovie).pipe(
tap(result => this.identifiedShows = [name, result]) tap(result => this._identifiedShows = [name, result])
); );
return this.identifying; return this._identifying;
} }
reIdentify(search: string) reIdentify(search: string): void
{ {
// TODO implement this
// this.identityShow(search).subscribe(x => this.identifyGrid.shows = x); // this.identityShow(search).subscribe(x => this.identifyGrid.shows = x);
} }
getMetadataID(provider: Provider) getMetadataID(provider: Provider): ExternalID
{ {
return this.show.externalIDs.find(x => x.provider.name == provider.name); return this.show.externalIDs.find(x => x.provider.name === provider.name);
} }
setMetadataID(provider: Provider, id: string, link: string = undefined) setMetadataID(provider: Provider, id: string, link: string = null): void
{ {
let i = this.show.externalIDs.findIndex(x => x.provider.name == provider.name); const i: number = this.show.externalIDs.findIndex(x => x.provider.name === provider.name);
this.metadataChanged = true; this.metadataChanged = true;
if (i != -1) if (i !== -1)
{ {
this.show.externalIDs[i].dataID = id; this.show.externalIDs[i].dataID = id;
this.show.externalIDs[i].link = link; this.show.externalIDs[i].link = link;
} }
else else
this.show.externalIDs.push({provider: provider, dataID: id, link: link}); this.show.externalIDs.push({provider, dataID: id, link});
} }
identifyID(show: Show) identifyID(show: Show): void
{ {
for (let id of show.externalIDs) for (const id of show.externalIDs)
this.setMetadataID(id.provider, id.dataID, id.link); this.setMetadataID(id.provider, id.dataID, id.link);
} }
} }

View File

@ -1,15 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component } from "@angular/core";
@Component({ @Component({
selector: 'app-not-found', selector: "app-not-found",
templateUrl: './not-found.component.html', templateUrl: "./not-found.component.html",
styleUrls: ['./not-found.component.scss'] styleUrls: ["./not-found.component.scss"]
}) })
export class NotFoundComponent implements OnInit { export class NotFoundComponent
{
constructor() { } constructor() { }
ngOnInit() {
}
} }

View File

@ -1,4 +1,4 @@
import { detect } from "detect-browser"; import { BotInfo, BrowserInfo, detect, NodeInfo, ReactNativeInfo, SearchBotDeviceInfo } from "detect-browser";
import { Track, WatchItem } from "../../models/watch-item"; import { Track, WatchItem } from "../../models/watch-item";
export enum method export enum method
@ -31,8 +31,8 @@ export class SupportList
export function getWhatIsSupported(player: HTMLVideoElement, item: WatchItem): SupportList export function getWhatIsSupported(player: HTMLVideoElement, item: WatchItem): SupportList
{ {
let supportList: SupportList = new SupportList(); const supportList: SupportList = new SupportList();
let browser = detect(); const browser: BrowserInfo | SearchBotDeviceInfo | BotInfo | NodeInfo | ReactNativeInfo = detect();
if (!browser) if (!browser)
{ {
@ -54,33 +54,33 @@ function containerIsSupported(player: HTMLVideoElement, container: string, brows
switch (container) switch (container)
{ {
case "asf": case "asf":
return browser == "tizen" || browser == "orsay" || browser == "edge"; return browser === "tizen" || browser === "orsay" || browser === "edge";
case "avi": case "avi":
return browser == "tizen" || browser == "orsay" || browser == "edge"; return browser === "tizen" || browser === "orsay" || browser === "edge";
case "mpg": case "mpg":
case "mpeg": case "mpeg":
return browser == "tizen" || browser == "orsay" || browser == "edge"; return browser === "tizen" || browser === "orsay" || browser === "edge";
case "flv": case "flv":
return browser == "tizen" || browser == "orsay"; return browser === "tizen" || browser === "orsay";
case "3gp": case "3gp":
case "mts": case "mts":
case "trp": case "trp":
case "vob": case "vob":
case "vro": case "vro":
return browser == "tizen" || browser == "orsay"; return browser === "tizen" || browser === "orsay";
case "mov": case "mov":
return browser == "tizen" || browser == "orsay" || browser == "edge" || browser == "chrome"; return browser === "tizen" || browser === "orsay" || browser === "edge" || browser === "chrome";
case "m2ts": case "m2ts":
return browser == "tizen" || browser == "orsay" || browser == "edge"; return browser === "tizen" || browser === "orsay" || browser === "edge";
case "wmv": case "wmv":
return browser == "tizen" || browser == "orsay" || browser == "edge"; return browser === "tizen" || browser === "orsay" || browser === "edge";
case "ts": case "ts":
return browser == "tizen" || browser == "orsay" || browser == "edge"; return browser === "tizen" || browser === "orsay" || browser === "edge";
case "mp4": case "mp4":
case "m4v": case "m4v":
return true; return true;
case "mkv": case "mkv":
if (browser == "tizen" || browser == "orsay" || browser == "chrome" || browser == "edge") if (browser === "tizen" || browser === "orsay" || browser === "chrome" || browser === "edge")
return true; return true;
return !!(player.canPlayType("video/x-matroska") || player.canPlayType("video/mkv")); return !!(player.canPlayType("video/x-matroska") || player.canPlayType("video/mkv"));
@ -95,34 +95,34 @@ function videoCodecIsSupported(player: HTMLVideoElement, codec: string, browser:
switch (codec) switch (codec)
{ {
case "h264": case "h264":
return !!player.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"'); return !!player.canPlayType("video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"");
case "h265": case "h265":
case "hevc": case "hevc":
if (browser == "tizen" || browser == "orsay" || browser == "xboxOne" || browser == "ios") if (browser === "tizen" || browser === "orsay" || browser === "xboxOne" || browser === "ios")
return true; return true;
// SHOULD SUPPORT CHROMECAST ULTRA // SHOULD SUPPORT CHROMECAST ULTRA
// if (browser.chromecast) // if (browser.chromecast)
// { // {
// var isChromecastUltra = userAgent.indexOf('aarch64') !== -1; // var isChromecastUltra = userAgent.indexOf('aarch64') !=== -1;
// if (isChromecastUltra) // if (isChromecastUltra)
// { // {
// return true; // return true;
// } // }
// } // }
return !!player.canPlayType('video/hevc; codecs="hevc, aac"'); return !!player.canPlayType("video/hevc; codecs=\"hevc, aac\"");
case "mpeg2video": case "mpeg2video":
return browser == "orsay" || browser == "tizen" || browser == "edge"; return browser === "orsay" || browser === "tizen" || browser === "edge";
case "vc1": case "vc1":
return browser == "orsay" || browser == "tizen" || browser == "edge"; return browser === "orsay" || browser === "tizen" || browser === "edge";
case "msmpeg4v2": case "msmpeg4v2":
return browser == "orsay" || browser == "tizen"; return browser === "orsay" || browser === "tizen";
case "vp8": case "vp8":
return !!player.canPlayType('video/webm; codecs="vp8'); return !!player.canPlayType("video/webm; codecs=\"vp8");
case "vp9": case "vp9":
return !!player.canPlayType('video/webm; codecs="vp9"'); return !!player.canPlayType("video/webm; codecs=\"vp9\"");
case "vorbis": case "vorbis":
return browser == "orsay" || browser == "tizen" || !!player.canPlayType('video/webm; codecs="vp8'); return browser === "orsay" || browser === "tizen" || !!player.canPlayType("video/webm; codecs=\"vp8");
default: default:
return false; return false;
} }
@ -134,21 +134,21 @@ function audioCodecIsSupported(player: HTMLVideoElement, codec: string, browser:
switch (codec) switch (codec)
{ {
case "mp3": case "mp3":
return !!player.canPlayType('video/mp4; codecs="avc1.640029, mp4a.69"') || return !!player.canPlayType("video/mp4; codecs=\"avc1.640029, mp4a.69\"") ||
!!player.canPlayType('video/mp4; codecs="avc1.640029, mp4a.6B"'); !!player.canPlayType("video/mp4; codecs=\"avc1.640029, mp4a.6B\"");
case "aac": case "aac":
return !!player.canPlayType('video/mp4; codecs="avc1.640029, mp4a.40.2"'); return !!player.canPlayType("video/mp4; codecs=\"avc1.640029, mp4a.40.2\"");
case "mp2": case "mp2":
return browser == "orsay" || browser == "tizen" || browser == "edge"; return browser === "orsay" || browser === "tizen" || browser === "edge";
case "pcm_s16le": case "pcm_s16le":
case "pcm_s24le": case "pcm_s24le":
return browser == "orsay" || browser == "tizen" || browser == "edge"; return browser === "orsay" || browser === "tizen" || browser === "edge";
case "aac_latm": case "aac_latm":
return browser == "orsay" || browser == "tizen"; return browser === "orsay" || browser === "tizen";
case "opus": case "opus":
return !!player.canPlayType('audio/ogg; codecs="opus"'); return !!player.canPlayType("audio/ogg; codecs=\"opus\"");
case "flac": case "flac":
return browser == "orsay" || browser == "tizen" || browser == "edge"; return browser === "orsay" || browser === "tizen" || browser === "edge";
default: default:
return false; return false;
} }

View File

@ -75,7 +75,7 @@
<div class="controller container-fluid" id="controller"> <div class="controller container-fluid" id="controller">
<div class="img d-none d-sm-block"> <div class="img d-none d-sm-block">
<img src="poster/{{this.item.showSlug}}" /> <img src="poster/{{this.item.showSlug}}" alt="poster" />
</div> </div>
<div class="content"> <div class="content">
<h3 *ngIf="!this.item.isMovie">S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3> <h3 *ngIf="!this.item.isMovie">S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3>
@ -112,7 +112,7 @@
<div id="next"> <div id="next">
<div id="main"> <div id="main">
<img src="{{this.item.nextEpisode.thumb}}" /> <img src="{{this.item.nextEpisode.thumb}}" alt="next episode thumbnail" />
</div> </div>
<div id="overview"> <div id="overview">
<h6>S{{this.item.nextEpisode.seasonNumber}}:E{{this.item.nextEpisode.episodeNumber}} - {{this.item.nextEpisode.title}}</h6> <h6>S{{this.item.nextEpisode.seasonNumber}}:E{{this.item.nextEpisode.episodeNumber}} - {{this.item.nextEpisode.title}}</h6>

View File

@ -24,7 +24,9 @@ import {
} from "./playbackMethodDetector"; } from "./playbackMethodDetector";
import { AppComponent } from "../../app.component"; import { AppComponent } from "../../app.component";
import { Track, WatchItem } from "../../models/watch-item"; import { Track, WatchItem } from "../../models/watch-item";
import SubtitlesOctopus from "libass-wasm/dist/js/subtitles-octopus.js" import SubtitlesOctopus from "libass-wasm/dist/js/subtitles-octopus.js";
import MouseMoveEvent = JQuery.MouseMoveEvent;
import TouchMoveEvent = JQuery.TouchMoveEvent;
@Pipe({ @Pipe({
@ -52,7 +54,7 @@ export class BufferToWidthPipe implements PipeTransform
{ {
transform(buffered: TimeRanges, duration: number): string transform(buffered: TimeRanges, duration: number): string
{ {
if (buffered.length == 0) if (buffered.length === 0)
return "0"; return "0";
return `${buffered.end(buffered.length - 1) / duration * 100}%`; return `${buffered.end(buffered.length - 1) / duration * 100}%`;
} }
@ -66,7 +68,7 @@ export class VolumeToButtonPipe implements PipeTransform
{ {
transform(volume: number, muted: boolean): string transform(volume: number, muted: boolean): string
{ {
if (volume == 0 || muted) if (volume === 0 || muted)
return "volume_off"; return "volume_off";
else if (volume < 25) else if (volume < 25)
return "volume_mute"; return "volume_mute";
@ -167,7 +169,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
private startup: StartupService) private startup: StartupService)
{ } { }
ngOnInit() ngOnInit(): void
{ {
document.getElementById("nav").classList.add("d-none"); document.getElementById("nav").classList.add("d-none");
if (AppComponent.isMobile) if (AppComponent.isMobile)
@ -223,7 +225,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
}); });
} }
ngOnDestroy() ngOnDestroy(): void
{ {
if (this.subtitlesManager) if (this.subtitlesManager)
this.subtitlesManager.dispose(); this.subtitlesManager.dispose();
@ -236,13 +238,13 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
$(document).off(); $(document).off();
} }
ngAfterViewInit() ngAfterViewInit(): void
{ {
if (this.oidcSecurity === undefined) if (this.oidcSecurity === undefined)
this.oidcSecurity = this.injector.get(OidcSecurityService); this.oidcSecurity = this.injector.get(OidcSecurityService);
this.hlsPlayer.config.xhrSetup = xhr => this.hlsPlayer.config.xhrSetup = xhr =>
{ {
const token = this.oidcSecurity.getToken(); const token: string = this.oidcSecurity.getToken();
if (token) if (token)
xhr.setRequestHeader("Authorization", "Bearer " + token); xhr.setRequestHeader("Authorization", "Bearer " + token);
}; };
@ -252,17 +254,17 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
setTimeout(() => this.route.data.subscribe(() => setTimeout(() => this.route.data.subscribe(() =>
{ {
// TODO remove the query param for the method (should be a session setting). // TODO remove the query param for the method (should be a session setting).
let queryMethod: string = this.route.snapshot.queryParams["method"]; const queryMethod: string = this.route.snapshot.queryParams.method;
this.supportList = getWhatIsSupported(this.player, this.item); this.supportList = getWhatIsSupported(this.player, this.item);
this.selectPlayMethod(queryMethod ? method[queryMethod] : this.supportList.getPlaybackMethod()); this.selectPlayMethod(queryMethod ? method[queryMethod] : this.supportList.getPlaybackMethod());
// TODO remove this, it should be a user's setting. // TODO remove this, it should be a user's setting.
const subSlug: string = this.route.snapshot.queryParams["sub"]; const subSlug: string = this.route.snapshot.queryParams.sub;
if (subSlug != null) if (subSlug != null)
{ {
const languageCode: string = subSlug.substring(0, 3); const languageCode: string = subSlug.substring(0, 3);
const forced: boolean = subSlug.length > 3 && subSlug.substring(4) == "for"; const forced: boolean = subSlug.length > 3 && subSlug.substring(4) === "for";
const sub: Track = this.item.subtitles.find(x => x.language == languageCode && x.isForced == forced); const sub: Track = this.item.subtitles.find(x => x.language === languageCode && x.isForced === forced);
this.selectSubtitle(sub, false); this.selectSubtitle(sub, false);
} }
})); }));
@ -278,7 +280,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
return AppComponent.isMobile; return AppComponent.isMobile;
} }
getTimeFromSeekbar(pageX: number) getTimeFromSeekbar(pageX: number): number
{ {
const value: number = (pageX - this.progressBar.offsetLeft) / this.progressBar.clientWidth; const value: number = (pageX - this.progressBar.offsetLeft) / this.progressBar.clientWidth;
const percent: number = Math.max(0, Math.min(value, 1)); const percent: number = Math.max(0, Math.min(value, 1));
@ -308,14 +310,14 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
} }
@HostListener("document:touchmove", ["$event"]) @HostListener("document:touchmove", ["$event"])
touchSeek(event) touchSeek(event: TouchMoveEvent): void
{ {
if (this.seeking) if (this.seeking)
this.player.currentTime = this.getTimeFromSeekbar(event.changedTouches[0].pageX); this.player.currentTime = this.getTimeFromSeekbar(event.changedTouches[0].pageX);
} }
@HostListener("document:mousemove", ["$event"]) @HostListener("document:mousemove", ["$event"])
mouseMove(event) mouseMove(event: MouseMoveEvent): void
{ {
if (this.seeking) if (this.seeking)
this.player.currentTime = this.getTimeFromSeekbar(event.pageX); this.player.currentTime = this.getTimeFromSeekbar(event.pageX);
@ -343,14 +345,14 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
} }
} }
selectPlayMethod(playMethod: method) selectPlayMethod(playMethod: method): void
{ {
this.playMethod = playMethod; this.playMethod = playMethod;
const url: string = [ const url: string = [
"/video", "/video",
this.playMethod.toLowerCase(), this.playMethod.toLowerCase(),
this.item.slug, this.item.slug,
this.playMethod != method.direct ? 'master.m3u8' : null this.playMethod !== method.direct ? "master.m3u8" : null
].filter(x => x !== null).join("/"); ].filter(x => x !== null).join("/");
if (this.playMethod === method.direct || this.player.canPlayType("application/vnd.apple.mpegurl")) if (this.playMethod === method.direct || this.player.canPlayType("application/vnd.apple.mpegurl"))
this.player.src = url; this.player.src = url;
@ -365,11 +367,11 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
} }
} }
back() back(): void
{ {
if (this.startup.loadedFromWatch) if (this.startup.loadedFromWatch)
{ {
this.router.navigate(["show", this.startup.show], {replaceUrl: true}) this.router.navigate(["show", this.startup.show], {replaceUrl: true});
this.startup.loadedFromWatch = false; this.startup.loadedFromWatch = false;
this.startup.show = null; this.startup.show = null;
} }
@ -377,7 +379,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
this.location.back(); this.location.back();
} }
next() next(): void
{ {
if (this.item.nextEpisode == null) if (this.item.nextEpisode == null)
return; return;
@ -387,7 +389,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
}); });
} }
previous() previous(): void
{ {
if (this.item.previousEpisode == null) if (this.item.previousEpisode == null)
return; return;
@ -397,7 +399,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
}); });
} }
videoClicked() videoClicked(): void
{ {
if (AppComponent.isMobile) if (AppComponent.isMobile)
this.showControls = !this.showControls; this.showControls = !this.showControls;
@ -408,7 +410,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
} }
} }
togglePlayback() togglePlayback(): void
{ {
if (this.player.paused) if (this.player.paused)
this.player.play(); this.player.play();
@ -416,7 +418,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
this.player.pause(); this.player.pause();
} }
fullscreen() fullscreen(): void
{ {
if (this.isFullScreen) if (this.isFullScreen)
document.exitFullscreen(); document.exitFullscreen();
@ -424,7 +426,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
document.body.requestFullscreen(); document.body.requestFullscreen();
} }
async selectSubtitle(subtitle: Track | number, changeUrl: boolean = true) async selectSubtitle(subtitle: Track | number, changeUrl: boolean = true): Promise<void>
{ {
if (typeof(subtitle) === "number") if (typeof(subtitle) === "number")
{ {
@ -444,7 +446,7 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
subSlug += "-for"; subSlug += "-for";
} }
this.router.navigate([], { await this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: {sub: subSlug}, queryParams: {sub: subSlug},
replaceUrl: true, replaceUrl: true,
@ -475,11 +477,11 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
}); });
this.removeHtmlTrack(); this.removeHtmlTrack();
if (subtitle.codec == "ass") if (subtitle.codec === "ass")
{ {
if (!this.subtitlesManager) if (!this.subtitlesManager)
{ {
let fonts: {[key: string]: string} = await this.shows.getFonts(this.item.showSlug).toPromise(); const fonts: { [key: string]: string } = await this.shows.getFonts(this.item.showSlug).toPromise();
this.subtitlesManager = new SubtitlesOctopus({ this.subtitlesManager = new SubtitlesOctopus({
video: this.player, video: this.player,
subUrl: `subtitle/${subtitle.slug}`, subUrl: `subtitle/${subtitle.slug}`,
@ -489,12 +491,12 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
else else
this.subtitlesManager.setTrackByUrl(`subtitle/${subtitle.slug}`); this.subtitlesManager.setTrackByUrl(`subtitle/${subtitle.slug}`);
} }
else if (subtitle.codec == "subrip") else if (subtitle.codec === "subrip")
{ {
if (this.subtitlesManager) if (this.subtitlesManager)
this.subtitlesManager.freeTrack(); this.subtitlesManager.freeTrack();
let track = document.createElement("track"); const track: HTMLTrackElement = document.createElement("track");
track.kind = "subtitles"; track.kind = "subtitles";
track.label = subtitle.displayName; track.label = subtitle.displayName;
track.srclang = subtitle.language; track.srclang = subtitle.language;
@ -510,9 +512,9 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
} }
} }
removeHtmlTrack() removeHtmlTrack(): void
{ {
let elements = this.player.getElementsByTagName("track"); const elements: HTMLCollectionOf<HTMLTrackElement> = this.player.getElementsByTagName("track");
if (elements.length > 0) if (elements.length > 0)
elements.item(0).remove(); elements.item(0).remove();
} }

View File

@ -1,21 +1,21 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, AfterViewInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { SearchResult } from "../../models/search-result"; import { SearchResult } from "../../models/search-result";
import { Title } from "@angular/platform-browser"; import { Title } from "@angular/platform-browser";
import { Page } from "../../models/page"; import { Page } from "../../models/page";
@Component({ @Component({
selector: 'app-search', selector: "app-search",
templateUrl: './search.component.html', templateUrl: "./search.component.html",
styleUrls: ['./search.component.scss'] styleUrls: ["./search.component.scss"]
}) })
export class SearchComponent implements OnInit, OnDestroy export class SearchComponent implements OnInit, OnDestroy, AfterViewInit
{ {
items: SearchResult; items: SearchResult;
constructor(private route: ActivatedRoute, private title: Title) { } constructor(private route: ActivatedRoute, private title: Title) { }
ngOnInit() ngOnInit(): void
{ {
this.route.data.subscribe((data) => this.route.data.subscribe((data) =>
{ {
@ -24,16 +24,16 @@ export class SearchComponent implements OnInit, OnDestroy
}); });
} }
ngAfterViewInit() ngAfterViewInit(): void
{ {
let searchBar: HTMLInputElement = <HTMLInputElement>document.getElementById("search"); const searchBar: HTMLInputElement = document.getElementById("search") as HTMLInputElement;
searchBar.classList.add("searching"); searchBar.classList.add("searching");
searchBar.value = this.items.query; searchBar.value = this.items.query;
} }
ngOnDestroy() ngOnDestroy(): void
{ {
let searchBar: HTMLInputElement = <HTMLInputElement>document.getElementById("search"); const searchBar: HTMLInputElement = document.getElementById("search") as HTMLInputElement;
searchBar.classList.remove("searching"); searchBar.classList.remove("searching");
searchBar.value = ""; searchBar.value = "";
} }

View File

@ -1,5 +1,5 @@
<div class="backdrop"> <div class="backdrop">
<img id="backdrop" src="backdrop/{{this.show.slug}}" /> <img id="backdrop" src="backdrop/{{this.show.slug}}" alt="backdrop" />
</div> </div>
<div class="header container pt-sm-5"> <div class="header container pt-sm-5">

View File

@ -1,21 +1,25 @@
import {Component, Inject} from '@angular/core'; import { Component, Inject } from "@angular/core";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import {DomSanitizer} from "@angular/platform-browser"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-trailer-dialog', selector: "app-trailer-dialog",
templateUrl: './trailer-dialog.component.html', templateUrl: "./trailer-dialog.component.html",
styleUrls: ['./trailer-dialog.component.scss'] styleUrls: ["./trailer-dialog.component.scss"]
}) })
export class TrailerDialogComponent export class TrailerDialogComponent
{ {
constructor(public dialogRef: MatDialogRef<TrailerDialogComponent>, public sanitizer: DomSanitizer, @Inject(MAT_DIALOG_DATA) public trailer: string) {} constructor(public dialogRef: MatDialogRef<TrailerDialogComponent>,
public sanitizer: DomSanitizer,
@Inject(MAT_DIALOG_DATA) public trailer: string)
{}
getYtTrailer() getYtTrailer(): SafeUrl
{ {
if (!this.trailer.includes("youtube.com")) if (!this.trailer.includes("youtube.com"))
return null; return null;
let uri = "https://www.youtube.com/embed/" + this.trailer.substring(this.trailer.indexOf("watch?v=") + 8) + "?autoplay=1"; const ytID: string = this.trailer.substring(this.trailer.indexOf("watch?v=") + 8);
const uri: string = `https://www.youtube.com/embed/${ytID}?autoplay=1`;
return this.sanitizer.bypassSecurityTrustResourceUrl(uri); return this.sanitizer.bypassSecurityTrustResourceUrl(uri);
} }
} }

View File

@ -1,6 +1,6 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import {Observable} from "rxjs" import { Observable } from "rxjs";
import { Page } from "../models/page"; import { Page } from "../models/page";
import { Genre } from "../models/resources/genre"; import { Genre } from "../models/resources/genre";
import { IResource } from "../models/resources/resource"; import { IResource } from "../models/resources/resource";
@ -34,7 +34,7 @@ class CrudApi<T extends IResource>
{ {
if (args == null) if (args == null)
return ""; return "";
let params: string = Object.keys(args).map(x => `${x}=${args[x]}`).join("&"); const params: string = Object.keys(args).map(x => `${x}=${args[x]}`).join("&");
return params ? `?${params}` : ""; return params ? `?${params}` : "";
} }
@ -67,7 +67,7 @@ class CrudApi<T extends IResource>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class LibraryService extends CrudApi<Library> export class LibraryService extends CrudApi<Library>
{ {
@ -78,7 +78,7 @@ export class LibraryService extends CrudApi<Library>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class LibraryItemService extends CrudApi<LibraryItem> export class LibraryItemService extends CrudApi<LibraryItem>
{ {
@ -89,7 +89,7 @@ export class LibraryItemService extends CrudApi<LibraryItem>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class SeasonService extends CrudApi<Season> export class SeasonService extends CrudApi<Season>
{ {
@ -106,7 +106,7 @@ export class SeasonService extends CrudApi<Season>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class EpisodeService extends CrudApi<Episode> export class EpisodeService extends CrudApi<Episode>
{ {
@ -129,7 +129,7 @@ export class EpisodeService extends CrudApi<Episode>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class PeopleService extends CrudApi<People> export class PeopleService extends CrudApi<People>
{ {
@ -146,7 +146,7 @@ export class PeopleService extends CrudApi<People>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class GenreService extends CrudApi<Genre> export class GenreService extends CrudApi<Genre>
{ {
@ -163,7 +163,7 @@ export class GenreService extends CrudApi<Genre>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class ShowService extends CrudApi<Show> export class ShowService extends CrudApi<Show>
{ {
@ -185,7 +185,7 @@ export class ShowService extends CrudApi<Show>
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class StudioService extends CrudApi<Studio> export class StudioService extends CrudApi<Studio>
{ {

View File

@ -10,7 +10,7 @@ export class ItemResolver
{ {
public static resolvers: any[] = []; public static resolvers: any[] = [];
static forResource<T>(resource: string) static forResource<T>(resource: string): any
{ {
@Injectable() @Injectable()
class Resolver implements Resolve<T> class Resolver implements Resolve<T>

View File

@ -1,9 +1,9 @@
import {HttpClient, HttpErrorResponse} from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import {Injectable} from '@angular/core'; import { Injectable } from "@angular/core";
import {MatSnackBar} from '@angular/material/snack-bar'; import { MatSnackBar } from "@angular/material/snack-bar";
import {ActivatedRouteSnapshot, Resolve} from '@angular/router'; import { ActivatedRouteSnapshot, Resolve } from "@angular/router";
import {Observable, EMPTY} from 'rxjs'; import { Observable, EMPTY } from "rxjs";
import {catchError, map} from 'rxjs/operators'; import { catchError, map } from "rxjs/operators";
import { Page } from "../models/page"; import { Page } from "../models/page";
import { IResource } from "../models/resources/resource"; import { IResource } from "../models/resources/resource";
@ -16,7 +16,7 @@ export class PageResolver
static forResource<T extends IResource>(resource: string, static forResource<T extends IResource>(resource: string,
copyParams: boolean | string[] | RouteMapper = false, copyParams: boolean | string[] | RouteMapper = false,
defaultQuery: string = null) defaultQuery: string = null): any
{ {
@Injectable() @Injectable()
class Resolver implements Resolve<Page<T>> class Resolver implements Resolve<Page<T>>
@ -27,23 +27,23 @@ export class PageResolver
resolve(route: ActivatedRouteSnapshot): Page<T> | Observable<Page<T>> | Promise<Page<T>> resolve(route: ActivatedRouteSnapshot): Page<T> | Observable<Page<T>> | Promise<Page<T>>
{ {
let res: string = resource.replace(/:([^:]*?)(\/|$|&)/g, (x, y, z) => `${route.paramMap.get(y)}${z}`); const res: string = resource.replace(/:([^:]*?)(\/|$|&)/g, (x, y, z) => `${route.paramMap.get(y)}${z}`);
let query: [string, string][] = defaultQuery const query: [string, string][] = defaultQuery
?.replace(/:([^:]*?)(\/|$|&)/g, (x, y, z) => `${route.paramMap.get(y)}${z}`) ?.replace(/:([^:]*?)(\/|$|&)/g, (x, y, z) => `${route.paramMap.get(y)}${z}`)
.split('&') .split("&")
.map(x => x.split('=') as [string, string]); .map(x => x.split("=") as [string, string]);
let uri: string; let uri: string;
if (typeof copyParams == "function") if (typeof copyParams === "function")
uri = copyParams(route, res, query); uri = copyParams(route, res, query);
else else
{ {
let entries: [string, string][] = copyParams == true const entries: [string, string][] = copyParams === true
? Object.entries(route.queryParams) ? Object.entries(route.queryParams)
: Object.entries(route.queryParams).filter(x => copyParams && copyParams.includes(x[0])); : Object.entries(route.queryParams).filter(x => copyParams && copyParams.includes(x[0]));
if (query) if (query)
entries.push(...query); entries.push(...query);
let params: string = entries.length > 0 const params: string = entries.length > 0
? '?' + entries.map(x => `${x[0]}=${x[1]}`).join('&') ? "?" + entries.map(x => `${x[0]}=${x[1]}`).join("&")
: ""; : "";
uri = `api/${res}${params}`; uri = `api/${res}${params}`;
} }
@ -56,7 +56,7 @@ export class PageResolver
console.log(error.status + " - " + error.message); console.log(error.status + " - " + error.message);
this.snackBar.open(`An unknown error occurred: ${error.message}.`, null, { this.snackBar.open(`An unknown error occurred: ${error.message}.`, null, {
horizontalPosition: "left", horizontalPosition: "left",
panelClass: ['snackError'], panelClass: ["snackError"],
duration: 2500 duration: 2500
}); });
return EMPTY; return EMPTY;

View File

@ -15,7 +15,7 @@ export class PreLoaderService
load<T>(route: string): Observable<T[]> load<T>(route: string): Observable<T[]>
{ {
let loaded = this.cache.find(x => x[0] === route); const loaded: [string, any[]] = this.cache.find(x => x[0] === route);
if (loaded != null) if (loaded != null)
return of(loaded[1]); return of(loaded[1]);
return this.http.get<Page<T>>(route).pipe(map(newData => return this.http.get<Page<T>>(route).pipe(map(newData =>

View File

@ -15,7 +15,7 @@ export class StartupService
if (window.location.pathname.startsWith("/watch/")) if (window.location.pathname.startsWith("/watch/"))
{ {
this.loadedFromWatch = true; this.loadedFromWatch = true;
this.show = window.location.pathname.match(/^\/watch\/(?<show>.*)(-s\d+e\d+)+?$/).groups["show"]; this.show = window.location.pathname.match(/^\/watch\/(?<show>.*)(-s\d+e\d+)+?$/).groups.show;
} }
return Promise.resolve(null); return Promise.resolve(null);
} }

View File

@ -1,3 +1,3 @@
export const environment = { export const environment: any = {
production: true production: true
}; };

View File

@ -2,7 +2,7 @@
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
export const environment = { export const environment: any = {
production: false production: false
}; };

View File

@ -1,8 +1,7 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
import { AppModule } from './app/app.module'; import { environment } from "./environments/environment";
import { environment } from './environments/environment';
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();

View File

@ -55,7 +55,7 @@
/*************************************************************************************************** /***************************************************************************************************
* Zone JS is required by default for Angular itself. * Zone JS is required by default for Angular itself.
*/ */
import 'zone.js/dist/zone'; // Included with Angular CLI. import "zone.js/dist/zone"; // Included with Angular CLI.
/*************************************************************************************************** /***************************************************************************************************

View File

@ -42,14 +42,12 @@
"trace" "trace"
], ],
"no-empty": false, "no-empty": false,
"no-inferrable-types": [ "no-inferrable-types": false,
true,
"ignore-params"
],
"no-non-null-assertion": true, "no-non-null-assertion": true,
"no-redundant-jsdoc": true, "no-redundant-jsdoc": true,
"jsdoc-format": false,
"no-switch-case-fall-through": true, "no-switch-case-fall-through": true,
"no-var-requires": false, "no-var-requires": true,
"object-literal-key-quotes": [ "object-literal-key-quotes": [
true, true,
"as-needed" "as-needed"
@ -74,7 +72,10 @@
}, },
"typedef": [ "typedef": [
true, true,
"call-signature" "call-signature",
"parameter",
"property-declaration",
"variable-declaration"
], ],
"typedef-whitespace": { "typedef-whitespace": {
"options": [ "options": [
@ -98,7 +99,8 @@
"options": [ "options": [
"ban-keywords", "ban-keywords",
"check-format", "check-format",
"allow-pascal-case" "allow-leading-underscore",
"require-const-for-all-caps"
] ]
}, },
"whitespace": { "whitespace": {