Using auto completion for people & studio and updating everything

This commit is contained in:
Zoe Roux 2020-10-06 22:20:16 +02:00
parent 33d8759e0f
commit c2efae048c
63 changed files with 1842 additions and 1500 deletions

2783
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,39 +9,40 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^9.1.12", "@angular/animations": "^10.1.4",
"@angular/cdk": "^9.2.4", "@angular/cdk": "^10.2.3",
"@angular/common": "^9.1.12", "@angular/common": "^10.1.4",
"@angular/compiler": "^9.1.12", "@angular/compiler": "^10.1.4",
"@angular/core": "^9.1.12", "@angular/core": "^10.1.4",
"@angular/forms": "^9.1.12", "@angular/forms": "^10.1.4",
"@angular/material": "^9.2.4", "@angular/material": "^10.2.3",
"@angular/platform-browser": "^9.1.12", "@angular/platform-browser": "^10.1.4",
"@angular/platform-browser-dynamic": "^9.1.12", "@angular/platform-browser-dynamic": "^10.1.4",
"@angular/router": "^9.1.12", "@angular/router": "^10.1.4",
"angular-auth-oidc-client": "10.0.14", "angular-auth-oidc-client": "11.2.0",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"detect-browser": "^5.1.1", "detect-browser": "^5.1.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"hls.js": "^0.13.2", "hls.js": "^0.14.13",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"ngx-infinite-scroll": "^9.1.0", "ngx-infinite-scroll": "^9.1.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"zone.js": "^0.10.3" "rxjs": "^6.6.3",
"zone.js": "^0.11.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.901.12", "@angular-devkit/build-angular": "^0.1001.4",
"@angular/cli": "^9.1.12", "@angular/cli": "^10.1.4",
"@angular/compiler-cli": "^9.1.12", "@angular/compiler-cli": "^10.1.4",
"@angular/language-service": "^9.1.12", "@angular/language-service": "^10.1.4",
"@types/bootstrap": "^4.5.0", "@types/bootstrap": "^4.5.0",
"@types/hls.js": "^0.12.6", "@types/hls.js": "^0.13.1",
"@types/jquery": "^3.5.1", "@types/jquery": "^3.5.1",
"@types/node": "^13.13.21", "@types/node": "^14.11.2",
"@types/video.js": "^7.3.11", "@types/video.js": "^7.3.11",
"codelyzer": "^5.2.2", "codelyzer": "^6.0.1",
"ts-node": "~8.6.2", "ts-node": "~9.0.0",
"tslint": "^5.0.0", "tslint": "^6.1.3",
"typescript": "~3.7.5" "typescript": "~4.0.3"
} }
} }

View File

@ -6,7 +6,7 @@ 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 {
EpisodeService, EpisodeService,
LibraryItemService, LibraryItemService,
@ -15,14 +15,14 @@ import {
SeasonService, SeasonService,
ShowService ShowService
} from "./services/api.service"; } from "./services/api.service";
import { Show } from "../models/resources/show"; import { Show } from "./models/resources/show";
import { ItemResolver } from "./services/item-resolver.service"; import { ItemResolver } from "./services/item-resolver.service";
import { CollectionComponent } from "./pages/collection/collection.component"; import { CollectionComponent } from "./pages/collection/collection.component";
import { Collection } from "../models/resources/collection"; import { Collection } from "./models/resources/collection";
import { SearchComponent } from "./pages/search/search.component"; import { SearchComponent } from "./pages/search/search.component";
import { SearchResult } from "../models/search-result"; import { SearchResult } from "./models/search-result";
import { PlayerComponent } from "./pages/player/player.component"; import { PlayerComponent } from "./pages/player/player.component";
import { WatchItem } from "../models/watch-item"; import { WatchItem } from "./models/watch-item";
const routes: Routes = [ const routes: Routes = [
{path: "browse", component: ItemsGridComponent, pathMatch: "full", {path: "browse", component: ItemsGridComponent, pathMatch: "full",

View File

@ -26,7 +26,15 @@
</li> </li>
<ng-template #accountDrop> <ng-template #accountDrop>
<li #accountParent class="nav-item icon" style="opacity: 1 !important;"> <li #accountParent class="nav-item icon" style="opacity: 1 !important;">
<img matRipple [src]="authManager.user.picture" [matMenuTriggerFor]="accountMenu" class="profilePicture" matTooltipPosition="below" [matTooltip]="authManager.user.username" fallback="more.svg" (error)="accountParent.style.removeProperty('opacity');" /> <img alt="Account"
matRipple
[src]="authManager.account.picture"
[matMenuTriggerFor]="accountMenu"
class="profilePicture"
matTooltipPosition="below"
[matTooltip]="authManager.account.username"
fallback="more.svg"
(error)="accountParent.style.removeProperty('opacity');" />
</li> </li>
<mat-menu #accountMenu="matMenu"> <mat-menu #accountMenu="matMenu">
<button class="dropButton" mat-menu-item (click)="this.openAccountDialog()">Settings</button> <button class="dropButton" mat-menu-item (click)="this.openAccountDialog()">Settings</button>

View File

@ -2,10 +2,9 @@ import {Component} from '@angular/core';
import {Event, Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError} from '@angular/router'; import {Event, Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError} from '@angular/router';
import {Location} from "@angular/common"; import {Location} from "@angular/common";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {Account} from "../models/account";
import {AccountComponent} from "./auth/account/account.component"; 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";
import * as $ from "jquery"; import * as $ from "jquery";
@ -77,11 +76,7 @@ export class AppComponent
openAccountDialog() openAccountDialog()
{ {
const dialog = this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.getAccount()}); this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.account});
dialog.afterClosed().subscribe((result: Account) =>
{
this.authManager.getUser();
});
} }
get isAuthenticated(): boolean get isAuthenticated(): boolean

View File

@ -1,7 +1,7 @@
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({
@ -14,7 +14,9 @@ export class AccountComponent
selectedPicture: File; selectedPicture: File;
@ViewChild("accountImg") accountImg: ElementRef; @ViewChild("accountImg") accountImg: ElementRef;
constructor(public dialogRef: MatDialogRef<AccountComponent>, @Inject(MAT_DIALOG_DATA) public account: Account, private http: HttpClient) {} constructor(public dialogRef: MatDialogRef<AccountComponent>,
@Inject(MAT_DIALOG_DATA) public account: Account,
private http: HttpClient) {}
finish() finish()
{ {

View File

@ -1,31 +1,49 @@
import {APP_INITIALIZER, NgModule} from '@angular/core'; import { CommonModule } from "@angular/common";
import {CommonModule} from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClient } from "@angular/common/http";
import {AccountComponent} from "./account/account.component"; import { APP_INITIALIZER, NgModule } from "@angular/core";
import {AuthPipe} from "./misc/auth.pipe"; import { FormsModule } from "@angular/forms";
import {UnauthorizedComponent} from "./unauthorized/unauthorized.component"; import { MatButtonModule } from "@angular/material/button";
import {LogoutComponent} from "./logout/logout.component"; import { MatCardModule } from "@angular/material/card";
import {ConfigResult, OidcConfigService, OidcSecurityService, OpenIdConfiguration, AuthModule as OidcModule} from "angular-auth-oidc-client"; import { MatCheckboxModule } from "@angular/material/checkbox";
import {HTTP_INTERCEPTORS, HttpClient} from "@angular/common/http"; import { MatRippleModule } from "@angular/material/core";
import {AuthGuard} from "./misc/authenticated-guard.service"; import { MatDialogModule } from "@angular/material/dialog";
import {AuthorizerInterceptor} from "./misc/authorizer-interceptor.service"; import { MatFormFieldModule } from "@angular/material/form-field";
import {MatFormFieldModule} from "@angular/material/form-field"; import { MatIconModule } from "@angular/material/icon";
import {MatIconModule} from "@angular/material/icon"; import { MatInputModule } from "@angular/material/input";
import {MatInputModule} from "@angular/material/input"; import { MatMenuModule } from "@angular/material/menu";
import {MatDialogModule} from "@angular/material/dialog"; import { MatSelectModule } from "@angular/material/select";
import {MatButtonModule} from "@angular/material/button"; import { MatSliderModule } from "@angular/material/slider";
import {MatSelectModule} from "@angular/material/select"; import { MatTabsModule } from "@angular/material/tabs";
import {MatMenuModule} from "@angular/material/menu"; import { MatTooltipModule } from "@angular/material/tooltip";
import {MatSliderModule} from "@angular/material/slider"; import { RouterModule } from "@angular/router";
import {MatTooltipModule} from "@angular/material/tooltip"; import { AuthModule as OidcModule, LogLevel, OidcConfigService } from "angular-auth-oidc-client";
import {MatRippleModule} from "@angular/material/core"; import { tap } from "rxjs/operators";
import {MatCardModule} from "@angular/material/card"; import { AccountComponent } from "./account/account.component";
import {FormsModule} from "@angular/forms"; import { LogoutComponent } from "./logout/logout.component";
import {MatTabsModule} from "@angular/material/tabs"; import { AuthPipe } from "./misc/auth.pipe";
import {MatCheckboxModule} from "@angular/material/checkbox"; import { AuthGuard } from "./misc/authenticated-guard.service";
import { AuthorizerInterceptor } from "./misc/authorizer-interceptor.service";
import { UnauthorizedComponent } from "./unauthorized/unauthorized.component";
export function loadConfig(oidcConfigService: OidcConfigService) export function loadConfig(oidcConfigService: OidcConfigService)
{ {
return () => oidcConfigService.load_using_stsServer(window.location.origin); return () => oidcConfigService.withConfig({
stsServer: window.location.origin,
redirectUrl: "/",
postLogoutRedirectUri: "/logout",
clientId: "kyoo.webapp",
responseType: "code",
triggerAuthorizationResultEvent: false,
scope: "openid profile offline_access",
silentRenew: true,
silentRenewUrl: "/silent.html",
useRefreshToken: true,
startCheckSession: true,
forbiddenRoute: "/forbidden",
unauthorizedRoute: "/unauthorized",
logLevel: LogLevel.Debug
});
} }
@NgModule({ @NgModule({
@ -51,7 +69,8 @@ export function loadConfig(oidcConfigService: OidcConfigService)
FormsModule, FormsModule,
MatTabsModule, MatTabsModule,
MatCheckboxModule, MatCheckboxModule,
OidcModule.forRoot() OidcModule.forRoot(),
RouterModule
], ],
entryComponents: [ entryComponents: [
AccountComponent AccountComponent
@ -74,32 +93,9 @@ export function loadConfig(oidcConfigService: OidcConfigService)
}) })
export class AuthModule export class AuthModule
{ {
constructor(private oidcSecurityService: OidcSecurityService, private oidcConfigService: OidcConfigService, http: HttpClient) constructor(http: HttpClient)
{ {
this.oidcConfigService.onConfigurationLoaded.subscribe((configResult: ConfigResult) => AuthGuard.permissionsObservable = http.get<string[]>("/api/account/default-permissions")
{ .pipe(tap(x => AuthGuard.defaultPermissions = x));
const config: OpenIdConfiguration = {
stsServer: configResult.customConfig.stsServer,
redirect_url: "/",
post_logout_redirect_uri: "/logout",
client_id: 'kyoo.webapp',
response_type: "code",
trigger_authorization_result_event: false,
scope: "openid profile",
silent_renew: true,
silent_renew_url: "/silent.html",
use_refresh_token: false,
start_checksession: true,
forbidden_route: '/Forbidden',
unauthorized_route: '/Unauthorized',
log_console_warning_active: true,
log_console_debug_active: true
};
this.oidcSecurityService.setupModule(config, configResult.authWellknownEndpoints);
});
http.get("/api/account/default-permissions").subscribe((result: string[]) => AuthGuard.defaultPermissions = result);
} }
} }

View File

@ -1,8 +1,7 @@
import {Injectable} from '@angular/core'; import { HttpClient } from "@angular/common/http";
import {AuthorizationResult, AuthorizationState, OidcSecurityService, ValidationResult} from "angular-auth-oidc-client"; import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http"; import { OidcSecurityService } from "angular-auth-oidc-client";
import {Account} from "../../models/account"; import { Account } from "../models/account";
import {Router} from "@angular/router";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -10,31 +9,23 @@ import {Router} from "@angular/router";
export class AuthService export class AuthService
{ {
isAuthenticated: boolean = false; isAuthenticated: boolean = false;
user: any; account: Account = null;
constructor(public oidcSecurityService: OidcSecurityService, private http: HttpClient, private router: Router)
{
if (this.oidcSecurityService.moduleSetup)
this.authorizeCallback();
else
this.oidcSecurityService.onModuleSetup.subscribe(() =>
{
this.authorizeCallback();
});
this.oidcSecurityService.onAuthorizationResult.subscribe((authorizationResult: AuthorizationResult) => constructor(private oidcSecurityService: OidcSecurityService,
{ private http: HttpClient)
this.getUser();
this.isAuthenticated = authorizationResult.authorizationState == AuthorizationState.authorized;
});
this.getUser();
}
getUser()
{ {
this.oidcSecurityService.getUserData().subscribe(userData => this.oidcSecurityService.checkAuth()
.subscribe((auth: boolean) => this.isAuthenticated = auth);
this.oidcSecurityService.userData$.subscribe(x =>
{ {
this.user = userData; if (x == null)
return;
this.account = {
email: x.email,
username: x.username,
picture: x.picture,
permissions: x.permissions.split(',')
};
}); });
} }
@ -45,30 +36,10 @@ export class AuthService
logout() logout()
{ {
document.cookie = "Authenticated=false; expires=" + new Date(2147483647 * 1000).toUTCString(); // this.http.get("api/account/logout").subscribe(() =>
this.http.get("api/account/logout").subscribe(() => // {
{
this.oidcSecurityService.logoff(); this.oidcSecurityService.logoff();
}); // document.cookie = "Authenticated=false; expires=" + new Date(2147483647 * 1000).toUTCString();
} // });
private authorizeCallback()
{
if (window.location.href.indexOf("?code=") != -1)
this.oidcSecurityService.authorizedCallbackWithCode(window.location.toString());
else if (window.location.href.indexOf("/login") == -1)
{
this.oidcSecurityService.getIsAuthorized().subscribe((authorized: boolean) =>
{
this.isAuthenticated = authorized;
});
}
}
getAccount(): Account
{
if (!this.isAuthenticated)
return null;
return {email: this.user.email, username: this.user.username, picture: this.user.picture};
} }
} }

View File

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

View File

@ -21,7 +21,7 @@ export class AuthPipe implements PipeTransform
const headers = new HttpHeaders({"Authorization": "Bearer " + token}); const headers = new HttpHeaders({"Authorization": "Bearer " + token});
const img = await this.http.get(uri, {headers, responseType: 'blob'}).toPromise(); const img = await this.http.get(uri, {headers, responseType: 'blob'}).toPromise();
const reader = new FileReader(); const reader = new FileReader();
return new Promise((resolve, reject) => { 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

@ -6,7 +6,6 @@ import {
UrlSegment, UrlSegment,
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree,
Router Router
} from '@angular/router'; } from '@angular/router';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
@ -17,6 +16,7 @@ export class AuthGuard
{ {
public static guards: any[] = []; public static guards: any[] = [];
public static defaultPermissions: string[]; public static defaultPermissions: string[];
public static permissionsObservable: Observable<string[]>;
static forPermissions(...permissions: string[]) static forPermissions(...permissions: string[])
{ {
@ -25,31 +25,31 @@ export class AuthGuard
{ {
constructor(private router: Router, private authManager: AuthService) {} constructor(private router: Router, private authManager: AuthService) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean>
{ {
if (!this.checkPermissions()) if (!await this.checkPermissions())
{ {
this.router.navigate(["/unauthorized"]); await this.router.navigate(["/unauthorized"]);
return false; return false;
} }
return true; return true;
} }
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean async canLoad(route: Route, segments: UrlSegment[]): Promise<boolean>
{ {
if (!this.checkPermissions()) if (!await this.checkPermissions())
{ {
this.router.navigate(["/unauthorized"]); await this.router.navigate(["/unauthorized"]);
return false; return false;
} }
return true; return true;
} }
checkPermissions(): boolean async checkPermissions(): Promise<boolean>
{ {
if (this.authManager.isAuthenticated) if (this.authManager.isAuthenticated)
{ {
let perms = this.authManager.user.permissions.split(","); const perms: string[] = this.authManager.account.permissions;
for (let perm of permissions) { for (let perm of permissions) {
if (!perms.includes(perm)) if (!perms.includes(perm))
return false; return false;
@ -58,8 +58,11 @@ export class AuthGuard
} }
else else
{ {
if (AuthGuard.defaultPermissions == undefined)
await AuthGuard.permissionsObservable.toPromise()
for (let perm of permissions) for (let perm of permissions)
if (AuthGuard.defaultPermissions?.includes(perm) === true) if (!AuthGuard.defaultPermissions.includes(perm))
return false; return false;
return true; return true;
} }

View File

@ -1,8 +1,8 @@
import { Component, Input} from '@angular/core'; import { Component, Input} from '@angular/core';
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { Episode } from "../../../models/resources/episode"; import { Episode } from "../../models/resources/episode";
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({

View File

@ -25,12 +25,13 @@
</mat-chip-list> </mat-chip-list>
</ng-container> </ng-container>
<ng-container *ngIf="this.studios.length > 0"> <br/>
<br/>
<h4><b>Studios</b></h4> <ng-container>
<mat-form-field class="w-100 px-3" (click)="$event.stopPropagation();"> <mat-form-field class="w-100 px-3" (click)="$event.stopPropagation();">
<input type="text" matInput [matAutocomplete]="auto" [formControl]="studioForm"> <mat-label>Studio</mat-label>
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" <input type="text" matInput [matAutocomplete]="autoStudio" [formControl]="studioForm" placeholder="None">
<mat-autocomplete autoActiveFirstOption #autoStudio="matAutocomplete"
(optionSelected)="this.addFilter('studio', $event.option.value, false)" (optionSelected)="this.addFilter('studio', $event.option.value, false)"
[displayWith]="this.nameGetter"> [displayWith]="this.nameGetter">
<mat-option *ngIf="this.shouldDisplayNoneStudio()" [value]="null">None</mat-option> <mat-option *ngIf="this.shouldDisplayNoneStudio()" [value]="null">None</mat-option>
@ -40,6 +41,35 @@
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
<ng-container>
<mat-form-field class="w-100 px-3" (click)="$event.stopPropagation();">
<mat-label>People</mat-label>
<mat-chip-list #peopleList>
<mat-chip *ngFor="let people of this.filters.people"
color="accent" selected
removable="true"
(removed)="this.addFilter('people', people)"
(click)="this.addFilter('people', people)">
{{people.name || people.slug}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input #peopleInput
[matAutocomplete]="autoPpl"
[matChipInputFor]="peopleList"
[formControl]="peopleForm"
(matChipInputTokenEnd)="this.addFilter('people', {id: 0, slug: $event.value});
$event.input.value = null;"/>
</mat-chip-list>
<mat-autocomplete #autoPpl="matAutocomplete"
(optionSelected)="this.addFilter('people', $event.option.value);
peopleInput.value = null;">
<mat-option *ngFor="let people of this.filteredPeople | async" [value]="people">
{{people.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</ng-container>
</mat-menu> </mat-menu>
<mat-menu #sortMenu="matMenu"> <mat-menu #sortMenu="matMenu">

View File

@ -21,7 +21,7 @@ button
.show .show
{ {
width: 27%; width: 27%;
min-width: 120px; min-width: 100px;
max-width: 168px; max-width: 168px;
list-style: none; list-style: none;
margin: .5em; margin: .5em;
@ -32,6 +32,7 @@ button
@include media-breakpoint-up(sm) @include media-breakpoint-up(sm)
{ {
width: 22%; width: 22%;
min-width: 120px;
} }
@include media-breakpoint-up(md) @include media-breakpoint-up(md)

View File

@ -2,18 +2,20 @@ 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 } 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";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { IResource } from "../../../models/resources/resource"; import { People } from "../../models/resources/people";
import { Show, ShowRole } from "../../../models/resources/show"; import { IResource } from "../../models/resources/resource";
import { Collection } from "../../../models/resources/collection"; import { Show, ShowRole } from "../../models/resources/show";
import { Studio } from "../../../models/resources/studio"; import { Collection } from "../../models/resources/collection";
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 { PreLoaderService } from "../../services/pre-loader.service"; import { PreLoaderService } from "../../services/pre-loader.service";
import { Observable } from "rxjs" import { Observable } from "rxjs"
import { map, startWith, tap } from "rxjs/operators" import { catchError, filter, map, mergeAll } from "rxjs/operators";
@Component({ @Component({
selector: 'app-items-grid', selector: 'app-items-grid',
@ -31,20 +33,25 @@ export class ItemsGridComponent implements OnInit
sortKeys: string[] = ["title", "start year", "end year"] sortKeys: string[] = ["title", "start year", "end year"]
sortUp: boolean = true; sortUp: boolean = true;
public static readonly showOnlyFilters: string[] = ["genres", "studio"] public static readonly showOnlyFilters: string[] = ["genres", "studio", "people"]
public static readonly filters: string[] = [].concat(...ItemsGridComponent.showOnlyFilters) public static readonly filters: string[] = [].concat(...ItemsGridComponent.showOnlyFilters)
filters: {genres: Genre[], studio: Studio} = {genres: [], studio: null}; filters: {genres: Genre[], studio: Studio, people: People[]} = {genres: [], studio: null, people: []};
genres: Genre[] = []; genres: Genre[] = [];
studios: Studio[] = [];
studioForm: FormControl = new FormControl(); studioForm: FormControl = new FormControl();
filteredStudios: Observable<Studio[]>; filteredStudios: Observable<Studio[]>;
peopleForm: FormControl = new FormControl();
filteredPeople: Observable<People[]>;
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private loader: PreLoaderService, private loader: PreLoaderService,
public client: HttpClient, private router: Router,
private router: Router) private studioApi: StudioService,
private peopleApi: PeopleService,
public client: HttpClient)
{ {
this.route.data.subscribe((data) => this.route.data.subscribe((data) =>
{ {
@ -60,11 +67,6 @@ export class ItemsGridComponent implements OnInit
this.genres = data; this.genres = data;
this.updateGenresFilterFromQuery(this.route.snapshot.queryParams); this.updateGenresFilterFromQuery(this.route.snapshot.queryParams);
}); });
this.loader.load<Studio>("/api/studios?limit=0").subscribe(data =>
{
this.studios = data;
this.updateStudioFilterFromQuery(this.route.snapshot.queryParams);
});
} }
updateGenresFilterFromQuery(query: Params) updateGenresFilterFromQuery(query: Params)
@ -82,17 +84,41 @@ export class ItemsGridComponent implements OnInit
updateStudioFilterFromQuery(query: Params) updateStudioFilterFromQuery(query: Params)
{ {
this.filters.studio = this.studios.find(x => x.slug == query.studio const slug: string = query.studio ?? this.route.snapshot.params.slug;
|| x.slug == this.route.snapshot.params.slug);
if (slug && this.filters.studio?.slug != slug)
{
this.filters.studio = {id: 0, slug: slug, name: slug};
this.studioApi.get(slug).subscribe(x => this.filters.studio = x);
}
} }
ngOnInit() ngOnInit()
{ {
this.filteredStudios = this.studioForm.valueChanges this.filteredStudios = this.studioForm.valueChanges
.pipe( .pipe(
map(x => x == null ? "" : x), filter(x => x),
map(x => typeof x === "string" ? x : x.name), map(x => typeof x === "string" ? x : x.name),
map(x => this.studios.filter(y => y.name.toLowerCase().indexOf(x.toLowerCase()) != -1)) map(x => this.studioApi.search(x)),
mergeAll(),
catchError(x =>
{
console.log(x);
return [];
})
);
this.filteredPeople = this.peopleForm.valueChanges
.pipe(
filter(x => x),
map(x => typeof x === "string" ? x : x.name),
map(x => this.peopleApi.search(x)),
mergeAll(),
catchError(x =>
{
console.log(x);
return [];
})
); );
} }
@ -102,6 +128,7 @@ export class ItemsGridComponent implements OnInit
} }
// TODO add /people to the switch list. // TODO add /people to the switch list.
// TODO only load studios & people when the user open the menu or load them from the server when typing.
/* /*
* /browse -> /api/items | /api/shows * /browse -> /api/items | /api/shows
@ -141,14 +168,14 @@ export class ItemsGridComponent implements OnInit
{ {
if (isArray) if (isArray)
{ {
if (this.filters[category].includes(filter)) if (this.filters[category].includes(filter) || this.filters[category].some(x => x.slug == filter.slug))
this.filters[category].splice(this.filters[category].indexOf(filter), 1); this.filters[category].splice(this.filters[category].indexOf(filter), 1);
else else
this.filters[category].push(filter); this.filters[category].push(filter);
} }
else else
{ {
if (this.filters[category] == filter) if (this.filters[category] == filter || this.filters[category]?.slug == filter.slug)
{ {
if (!toggle) if (!toggle)
return; return;
@ -184,6 +211,14 @@ export class ItemsGridComponent implements OnInit
}); });
return; return;
} }
if (this.filters.people.length == 1 && this.getFilterCount() == 1)
{
this.router.navigate(["people", this.filters.people[0].slug], {
replaceUrl: true,
queryParams: {sortBy: this.route.snapshot.queryParams.sortBy}
});
return;
}
if (this.getFilterCount() == 0 || this.router.url != "/browse") if (this.getFilterCount() == 0 || this.router.url != "/browse")
{ {
let params = {[category]: param} let params = {[category]: param}
@ -191,6 +226,8 @@ export class ItemsGridComponent implements OnInit
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")
params.people = `${this.route.snapshot.params.slug}`;
this.router.navigate(["/browse"], { this.router.navigate(["/browse"], {
queryParams: params, queryParams: params,

View File

@ -1,11 +1,11 @@
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} 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";
import {Show, ShowRole} from "../../../models/resources/show"; import {Show, ShowRole} from "../../models/resources/show";
import {LibraryItem} from "../../../models/resources/library-item"; import {LibraryItem} from "../../models/resources/library-item";
import {ItemsUtils} from "../../misc/items-utils"; import {ItemsUtils} from "../../misc/items-utils";
@Component({ @Component({
@ -36,4 +36,4 @@ export class ItemsListComponent extends HorizontalScroller
{ {
return ItemsUtils.getLink(item); return ItemsUtils.getLink(item);
} }
} }

View File

@ -1,9 +1,9 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { MatButton } from "@angular/material/button"; import { MatButton } from "@angular/material/button";
import { DomSanitizer } 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({

View File

@ -1,7 +1,7 @@
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DomSanitizer} from "@angular/platform-browser"; import {DomSanitizer} 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',

View File

@ -1,7 +1,7 @@
import {ItemType, LibraryItem} from "../../models/resources/library-item"; import {ItemType, LibraryItem} from "../models/resources/library-item";
import {Show, ShowRole} from "../../models/resources/show"; import {Show, ShowRole} from "../models/resources/show";
import {Collection} from "../../models/resources/collection"; import {Collection} from "../models/resources/collection";
import {People} from "../../models/resources/people"; import {People} from "../models/resources/people";
export class ItemsUtils export class ItemsUtils
{ {
@ -30,4 +30,4 @@ export class ItemsUtils
return `${item.startYear} - ${item.endYear}` return `${item.startYear} - ${item.endYear}`
return item.startYear?.toString(); return item.startYear?.toString();
} }
} }

View File

@ -3,4 +3,5 @@ export interface Account
username: string; username: string;
email: string; email: string;
picture: string; picture: string;
permissions: string[];
} }

View File

@ -1,11 +1,11 @@
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} 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";
import {LibraryItem} from "../../../models/resources/library-item"; import {LibraryItem} from "../../models/resources/library-item";
import {ItemsUtils} from "../../misc/items-utils"; import {ItemsUtils} from "../../misc/items-utils";
@Component({ @Component({

View File

@ -1,14 +1,14 @@
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 {Show} from "../../../models/resources/show"; import {Show} from "../../models/resources/show";
import {Genre} from "../../../models/resources/genre"; import {Genre} from "../../models/resources/genre";
import {MatChipInputEvent} from "@angular/material/chips"; import {MatChipInputEvent} from "@angular/material/chips";
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete"; import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
import {Observable, of} from "rxjs"; import {Observable, of} from "rxjs";
import {tap} from "rxjs/operators"; import {tap} from "rxjs/operators";
import {Studio} from "../../../models/resources/studio"; import {Studio} from "../../models/resources/studio";
import {Provider} from "../../../models/provider"; 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";

View File

@ -2,7 +2,7 @@ import {Component, Injector, OnInit, ViewEncapsulation} from '@angular/core';
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {DomSanitizer, Title} from "@angular/platform-browser"; import {DomSanitizer, Title} from "@angular/platform-browser";
import {ActivatedRoute, Event, NavigationCancel, NavigationEnd, NavigationStart, Router} from "@angular/router"; import {ActivatedRoute, Event, NavigationCancel, NavigationEnd, NavigationStart, Router} from "@angular/router";
import {Track, WatchItem} from "../../../models/watch-item"; import {Track, WatchItem} from "../../models/watch-item";
import {Location} from "@angular/common"; import {Location} from "@angular/common";
import * as Hls from "hls.js" import * as Hls from "hls.js"
import {getPlaybackMethod, getWhatIsSupported, method, SupportList} from "../../../videoSupport/playbackMethodDetector"; import {getPlaybackMethod, getWhatIsSupported, method, SupportList} from "../../../videoSupport/playbackMethodDetector";

View File

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } 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',

View File

@ -2,15 +2,15 @@ import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from "@angular/material/snack-bar"; import { MatSnackBar } from "@angular/material/snack-bar";
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { Episode } from "../../../models/resources/episode"; import { Episode } from "../../models/resources/episode";
import { Show } from "../../../models/resources/show"; import { Show } from "../../models/resources/show";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {TrailerDialogComponent} from "../trailer-dialog/trailer-dialog.component"; import {TrailerDialogComponent} from "../trailer-dialog/trailer-dialog.component";
import {MetadataEditComponent} from "../metadata-edit/metadata-edit.component"; import {MetadataEditComponent} from "../metadata-edit/metadata-edit.component";
import {Season} from "../../../models/resources/season"; import {Season} from "../../models/resources/season";
import {EpisodeService, PeopleService, SeasonService} from "../../services/api.service"; import {EpisodeService, PeopleService, SeasonService} from "../../services/api.service";
import {Page} from "../../../models/page"; import {Page} from "../../models/page";
import {People} from "../../../models/resources/people"; import {People} from "../../models/resources/people";
@Component({ @Component({
selector: 'app-show-details', selector: 'app-show-details',

View File

@ -1,15 +1,16 @@
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 {IResource} from "../../models/resources/resource"; import {IResource} from "../models/resources/resource";
import {Library} from "../../models/resources/library"; import {Library} from "../models/resources/library";
import {LibraryItem} from "../../models/resources/library-item"; import {LibraryItem} from "../models/resources/library-item";
import {map} from "rxjs/operators"; import {map} from "rxjs/operators";
import {Season} from "../../models/resources/season"; import {Season} from "../models/resources/season";
import {Episode} from "../../models/resources/episode"; import {Episode} from "../models/resources/episode";
import {People} from "../../models/resources/people"; import {People} from "../models/resources/people";
import {Show} from "../../models/resources/show"; import {Show} from "../models/resources/show";
import { Studio } from "../models/resources/studio";
export interface ApiArgs export interface ApiArgs
{ {
@ -57,6 +58,11 @@ class CrudApi<T extends IResource>
{ {
return this.client.delete<T>(`/api/${this.route}/${item.slug}`); return this.client.delete<T>(`/api/${this.route}/${item.slug}`);
} }
search(name: string): Observable<T[]>
{
return this.client.get<T[]>(`/api/search/${name}/${this.route}`);
}
} }
@Injectable({ @Injectable({
@ -155,3 +161,19 @@ export class ShowService extends CrudApi<Show>
} }
} }
@Injectable({
providedIn: 'root'
})
export class StudioService extends CrudApi<Studio>
{
constructor(client: HttpClient)
{
super(client, "studios");
}
getForShow(show: string | number) : Observable<Studio>
{
return this.client.get<Studio>(`/api/show/${show}/studio}`);
}
}

View File

@ -4,8 +4,8 @@ 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";
type RouteMapper = (route: ActivatedRouteSnapshot, endpoint: string, queryParams: [string, string][]) => string; type RouteMapper = (route: ActivatedRouteSnapshot, endpoint: string, queryParams: [string, string][]) => string;

View File

@ -1,6 +1,6 @@
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Page } from "../../models/page"; import { Page } from "../models/page";
import { Observable, of } from "rxjs" import { Observable, of } from "rxjs"
import { map } from "rxjs/operators" import { map } from "rxjs/operators"

View File

@ -1,5 +1,5 @@
import { detect } from "detect-browser"; import { detect } from "detect-browser";
import { Track, WatchItem } from "../models/watch-item"; import { Track, WatchItem } from "../app/models/watch-item";
export enum method export enum method
{ {

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<base href="./" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Silent Renew</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
<script>
window.onload = function() {
/* The parent window hosts the Angular application */
var parent = window.parent;
/* Send the id_token information to the oidc message handler */
var event = new CustomEvent('oidc-silent-renew-message', { detail: window.location });
parent.dispatchEvent(event);
};
</script>
</body>
</html>