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,
"dependencies": {
"@angular/animations": "^9.1.12",
"@angular/cdk": "^9.2.4",
"@angular/common": "^9.1.12",
"@angular/compiler": "^9.1.12",
"@angular/core": "^9.1.12",
"@angular/forms": "^9.1.12",
"@angular/material": "^9.2.4",
"@angular/platform-browser": "^9.1.12",
"@angular/platform-browser-dynamic": "^9.1.12",
"@angular/router": "^9.1.12",
"angular-auth-oidc-client": "10.0.14",
"@angular/animations": "^10.1.4",
"@angular/cdk": "^10.2.3",
"@angular/common": "^10.1.4",
"@angular/compiler": "^10.1.4",
"@angular/core": "^10.1.4",
"@angular/forms": "^10.1.4",
"@angular/material": "^10.2.3",
"@angular/platform-browser": "^10.1.4",
"@angular/platform-browser-dynamic": "^10.1.4",
"@angular/router": "^10.1.4",
"angular-auth-oidc-client": "11.2.0",
"bootstrap": "^4.5.2",
"detect-browser": "^5.1.1",
"hammerjs": "^2.0.8",
"hls.js": "^0.13.2",
"hls.js": "^0.14.13",
"jquery": "^3.5.1",
"ngx-infinite-scroll": "^9.1.0",
"popper.js": "^1.16.1",
"zone.js": "^0.10.3"
"rxjs": "^6.6.3",
"zone.js": "^0.11.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.901.12",
"@angular/cli": "^9.1.12",
"@angular/compiler-cli": "^9.1.12",
"@angular/language-service": "^9.1.12",
"@angular-devkit/build-angular": "^0.1001.4",
"@angular/cli": "^10.1.4",
"@angular/compiler-cli": "^10.1.4",
"@angular/language-service": "^10.1.4",
"@types/bootstrap": "^4.5.0",
"@types/hls.js": "^0.12.6",
"@types/hls.js": "^0.13.1",
"@types/jquery": "^3.5.1",
"@types/node": "^13.13.21",
"@types/node": "^14.11.2",
"@types/video.js": "^7.3.11",
"codelyzer": "^5.2.2",
"ts-node": "~8.6.2",
"tslint": "^5.0.0",
"typescript": "~3.7.5"
"codelyzer": "^6.0.1",
"ts-node": "~9.0.0",
"tslint": "^6.1.3",
"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 { ShowDetailsComponent } from './pages/show-details/show-details.component';
import { AuthGuard } from "./auth/misc/authenticated-guard.service";
import { LibraryItem } from "../models/resources/library-item";
import { LibraryItem } from "./models/resources/library-item";
import {
EpisodeService,
LibraryItemService,
@ -15,14 +15,14 @@ import {
SeasonService,
ShowService
} from "./services/api.service";
import { Show } from "../models/resources/show";
import { Show } from "./models/resources/show";
import { ItemResolver } from "./services/item-resolver.service";
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 { SearchResult } from "../models/search-result";
import { SearchResult } from "./models/search-result";
import { PlayerComponent } from "./pages/player/player.component";
import { WatchItem } from "../models/watch-item";
import { WatchItem } from "./models/watch-item";
const routes: Routes = [
{path: "browse", component: ItemsGridComponent, pathMatch: "full",

View File

@ -26,7 +26,15 @@
</li>
<ng-template #accountDrop>
<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>
<mat-menu #accountMenu="matMenu">
<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 {Location} from "@angular/common";
import {MatDialog} from "@angular/material/dialog";
import {Account} from "../models/account";
import {AccountComponent} from "./auth/account/account.component";
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 * as $ from "jquery";
@ -77,11 +76,7 @@ export class AppComponent
openAccountDialog()
{
const dialog = this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.getAccount()});
dialog.afterClosed().subscribe((result: Account) =>
{
this.authManager.getUser();
});
this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.account});
}
get isAuthenticated(): boolean

View File

@ -1,7 +1,7 @@
import {Component, ElementRef, Inject, ViewChild} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {Account} from "../../../models/account";
import {Account} from "../../models/account";
@Component({
@ -14,7 +14,9 @@ export class AccountComponent
selectedPicture: File;
@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()
{

View File

@ -1,31 +1,49 @@
import {APP_INITIALIZER, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AccountComponent} from "./account/account.component";
import {AuthPipe} from "./misc/auth.pipe";
import {UnauthorizedComponent} from "./unauthorized/unauthorized.component";
import {LogoutComponent} from "./logout/logout.component";
import {ConfigResult, OidcConfigService, OidcSecurityService, OpenIdConfiguration, AuthModule as OidcModule} from "angular-auth-oidc-client";
import {HTTP_INTERCEPTORS, HttpClient} from "@angular/common/http";
import {AuthGuard} from "./misc/authenticated-guard.service";
import {AuthorizerInterceptor} from "./misc/authorizer-interceptor.service";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatIconModule} from "@angular/material/icon";
import {MatInputModule} from "@angular/material/input";
import {MatDialogModule} from "@angular/material/dialog";
import {MatButtonModule} from "@angular/material/button";
import {MatSelectModule} from "@angular/material/select";
import {MatMenuModule} from "@angular/material/menu";
import {MatSliderModule} from "@angular/material/slider";
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatRippleModule} from "@angular/material/core";
import {MatCardModule} from "@angular/material/card";
import {FormsModule} from "@angular/forms";
import {MatTabsModule} from "@angular/material/tabs";
import {MatCheckboxModule} from "@angular/material/checkbox";
import { CommonModule } from "@angular/common";
import { HTTP_INTERCEPTORS, HttpClient } from "@angular/common/http";
import { APP_INITIALIZER, NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from "@angular/material/card";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatRippleModule } from "@angular/material/core";
import { MatDialogModule } from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatMenuModule } from "@angular/material/menu";
import { MatSelectModule } from "@angular/material/select";
import { MatSliderModule } from "@angular/material/slider";
import { MatTabsModule } from "@angular/material/tabs";
import { MatTooltipModule } from "@angular/material/tooltip";
import { RouterModule } from "@angular/router";
import { AuthModule as OidcModule, LogLevel, OidcConfigService } from "angular-auth-oidc-client";
import { tap } from "rxjs/operators";
import { AccountComponent } from "./account/account.component";
import { LogoutComponent } from "./logout/logout.component";
import { AuthPipe } from "./misc/auth.pipe";
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)
{
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({
@ -51,7 +69,8 @@ export function loadConfig(oidcConfigService: OidcConfigService)
FormsModule,
MatTabsModule,
MatCheckboxModule,
OidcModule.forRoot()
OidcModule.forRoot(),
RouterModule
],
entryComponents: [
AccountComponent
@ -74,32 +93,9 @@ export function loadConfig(oidcConfigService: OidcConfigService)
})
export class AuthModule
{
constructor(private oidcSecurityService: OidcSecurityService, private oidcConfigService: OidcConfigService, http: HttpClient)
constructor(http: HttpClient)
{
this.oidcConfigService.onConfigurationLoaded.subscribe((configResult: ConfigResult) =>
{
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);
AuthGuard.permissionsObservable = http.get<string[]>("/api/account/default-permissions")
.pipe(tap(x => AuthGuard.defaultPermissions = x));
}
}

View File

@ -1,8 +1,7 @@
import {Injectable} from '@angular/core';
import {AuthorizationResult, AuthorizationState, OidcSecurityService, ValidationResult} from "angular-auth-oidc-client";
import {HttpClient} from "@angular/common/http";
import {Account} from "../../models/account";
import {Router} from "@angular/router";
import { HttpClient } from "@angular/common/http";
import { Injectable } from '@angular/core';
import { OidcSecurityService } from "angular-auth-oidc-client";
import { Account } from "../models/account";
@Injectable({
providedIn: 'root'
@ -10,31 +9,23 @@ import {Router} from "@angular/router";
export class AuthService
{
isAuthenticated: boolean = false;
user: any;
account: Account = null;
constructor(public oidcSecurityService: OidcSecurityService, private http: HttpClient, private router: Router)
constructor(private oidcSecurityService: OidcSecurityService,
private http: HttpClient)
{
if (this.oidcSecurityService.moduleSetup)
this.authorizeCallback();
else
this.oidcSecurityService.onModuleSetup.subscribe(() =>
{
this.authorizeCallback();
});
this.oidcSecurityService.onAuthorizationResult.subscribe((authorizationResult: AuthorizationResult) =>
this.oidcSecurityService.checkAuth()
.subscribe((auth: boolean) => this.isAuthenticated = auth);
this.oidcSecurityService.userData$.subscribe(x =>
{
this.getUser();
this.isAuthenticated = authorizationResult.authorizationState == AuthorizationState.authorized;
});
this.getUser();
}
getUser()
{
this.oidcSecurityService.getUserData().subscribe(userData =>
{
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()
{
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();
});
}
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};
// document.cookie = "Authenticated=false; expires=" + new Date(2147483647 * 1000).toUTCString();
// });
}
}

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { Component, Input} from '@angular/core';
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 {Page} from "../../../models/page";
import {Page} from "../../models/page";
import {HttpClient} from "@angular/common/http";
@Component({

View File

@ -25,12 +25,13 @@
</mat-chip-list>
</ng-container>
<ng-container *ngIf="this.studios.length > 0">
<br/>
<h4><b>Studios</b></h4>
<br/>
<ng-container>
<mat-form-field class="w-100 px-3" (click)="$event.stopPropagation();">
<input type="text" matInput [matAutocomplete]="auto" [formControl]="studioForm">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete"
<mat-label>Studio</mat-label>
<input type="text" matInput [matAutocomplete]="autoStudio" [formControl]="studioForm" placeholder="None">
<mat-autocomplete autoActiveFirstOption #autoStudio="matAutocomplete"
(optionSelected)="this.addFilter('studio', $event.option.value, false)"
[displayWith]="this.nameGetter">
<mat-option *ngIf="this.shouldDisplayNoneStudio()" [value]="null">None</mat-option>
@ -40,6 +41,35 @@
</mat-autocomplete>
</mat-form-field>
</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 #sortMenu="matMenu">

View File

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

View File

@ -2,18 +2,20 @@ import { Component, Input, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { ActivatedRoute, ActivatedRouteSnapshot, Params, Router } from "@angular/router";
import { DomSanitizer } from '@angular/platform-browser';
import { Genre } from "../../../models/resources/genre";
import { LibraryItem } from "../../../models/resources/library-item";
import { Page } from "../../../models/page";
import { Genre } from "../../models/resources/genre";
import { LibraryItem } from "../../models/resources/library-item";
import { Page } from "../../models/page";
import { HttpClient } from "@angular/common/http";
import { IResource } from "../../../models/resources/resource";
import { Show, ShowRole } from "../../../models/resources/show";
import { Collection } from "../../../models/resources/collection";
import { Studio } from "../../../models/resources/studio";
import { People } from "../../models/resources/people";
import { IResource } from "../../models/resources/resource";
import { Show, ShowRole } from "../../models/resources/show";
import { Collection } from "../../models/resources/collection";
import { Studio } from "../../models/resources/studio";
import { ItemsUtils } from "../../misc/items-utils";
import { PeopleService, StudioService } from "../../services/api.service";
import { PreLoaderService } from "../../services/pre-loader.service";
import { Observable } from "rxjs"
import { map, startWith, tap } from "rxjs/operators"
import { catchError, filter, map, mergeAll } from "rxjs/operators";
@Component({
selector: 'app-items-grid',
@ -31,20 +33,25 @@ export class ItemsGridComponent implements OnInit
sortKeys: string[] = ["title", "start year", "end year"]
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)
filters: {genres: Genre[], studio: Studio} = {genres: [], studio: null};
filters: {genres: Genre[], studio: Studio, people: People[]} = {genres: [], studio: null, people: []};
genres: Genre[] = [];
studios: Studio[] = [];
studioForm: FormControl = new FormControl();
filteredStudios: Observable<Studio[]>;
peopleForm: FormControl = new FormControl();
filteredPeople: Observable<People[]>;
constructor(private route: ActivatedRoute,
private sanitizer: DomSanitizer,
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) =>
{
@ -60,11 +67,6 @@ export class ItemsGridComponent implements OnInit
this.genres = data;
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)
@ -82,17 +84,41 @@ export class ItemsGridComponent implements OnInit
updateStudioFilterFromQuery(query: Params)
{
this.filters.studio = this.studios.find(x => x.slug == query.studio
|| x.slug == this.route.snapshot.params.slug);
const slug: string = query.studio ?? 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()
{
this.filteredStudios = this.studioForm.valueChanges
.pipe(
map(x => x == null ? "" : x),
filter(x => x),
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 only load studios & people when the user open the menu or load them from the server when typing.
/*
* /browse -> /api/items | /api/shows
@ -141,14 +168,14 @@ export class ItemsGridComponent implements OnInit
{
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);
else
this.filters[category].push(filter);
}
else
{
if (this.filters[category] == filter)
if (this.filters[category] == filter || this.filters[category]?.slug == filter.slug)
{
if (!toggle)
return;
@ -184,6 +211,14 @@ export class ItemsGridComponent implements OnInit
});
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")
{
let params = {[category]: param}
@ -191,6 +226,8 @@ export class ItemsGridComponent implements OnInit
params.studio = this.route.snapshot.params.slug;
if (this.router.url.startsWith("/genre") && category != "genres")
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"], {
queryParams: params,

View File

@ -1,11 +1,11 @@
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 {HorizontalScroller} from "../../misc/horizontal-scroller";
import {Page} from "../../../models/page";
import {Page} from "../../models/page";
import {HttpClient} from "@angular/common/http";
import {Show, ShowRole} from "../../../models/resources/show";
import {LibraryItem} from "../../../models/resources/library-item";
import {Show, ShowRole} from "../../models/resources/show";
import {LibraryItem} from "../../models/resources/library-item";
import {ItemsUtils} from "../../misc/items-utils";
@Component({

View File

@ -1,9 +1,9 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { MatButton } from "@angular/material/button";
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 {Page} from "../../../models/page";
import {Page} from "../../models/page";
import {HttpClient} from "@angular/common/http";
@Component({

View File

@ -1,7 +1,7 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DomSanitizer} from "@angular/platform-browser";
import {Show} from "../../../models/resources/show";
import {Page} from "../../../models/page";
import {Show} from "../../models/resources/show";
import {Page} from "../../models/page";
@Component({
selector: 'app-shows-grid',

View File

@ -1,7 +1,7 @@
import {ItemType, LibraryItem} from "../../models/resources/library-item";
import {Show, ShowRole} from "../../models/resources/show";
import {Collection} from "../../models/resources/collection";
import {People} from "../../models/resources/people";
import {ItemType, LibraryItem} from "../models/resources/library-item";
import {Show, ShowRole} from "../models/resources/show";
import {Collection} from "../models/resources/collection";
import {People} from "../models/resources/people";
export class ItemsUtils
{

View File

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

View File

@ -1,11 +1,11 @@
import {Component} from '@angular/core';
import {Collection} from "../../../models/resources/collection";
import {Collection} from "../../models/resources/collection";
import {ActivatedRoute} from "@angular/router";
import {DomSanitizer} from "@angular/platform-browser";
import {Show, ShowRole} from "../../../models/resources/show";
import {Page} from "../../../models/page";
import {People} from "../../../models/resources/people";
import {LibraryItem} from "../../../models/resources/library-item";
import {Show, ShowRole} from "../../models/resources/show";
import {Page} from "../../models/page";
import {People} from "../../models/resources/people";
import {LibraryItem} from "../../models/resources/library-item";
import {ItemsUtils} from "../../misc/items-utils";
@Component({

View File

@ -1,14 +1,14 @@
import {Component, ElementRef, Inject, ViewChild} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {Show} from "../../../models/resources/show";
import {Genre} from "../../../models/resources/genre";
import {Show} from "../../models/resources/show";
import {Genre} from "../../models/resources/genre";
import {MatChipInputEvent} from "@angular/material/chips";
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
import {Observable, of} from "rxjs";
import {tap} from "rxjs/operators";
import {Studio} from "../../../models/resources/studio";
import {Provider} from "../../../models/provider";
import {Studio} from "../../models/resources/studio";
import {Provider} from "../../models/provider";
import {MatSnackBar} from "@angular/material/snack-bar";
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 {DomSanitizer, Title} from "@angular/platform-browser";
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 * as Hls from "hls.js"
import {getPlaybackMethod, getWhatIsSupported, method, SupportList} from "../../../videoSupport/playbackMethodDetector";

View File

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import { SearchResult } from "../../../models/search-result";
import { SearchResult } from "../../models/search-result";
import { Title } from "@angular/platform-browser";
import {Page} from "../../../models/page";
import {Page} from "../../models/page";
@Component({
selector: 'app-search',

View File

@ -2,15 +2,15 @@ import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from "@angular/material/snack-bar";
import { Title } from '@angular/platform-browser';
import {ActivatedRoute, Router} from '@angular/router';
import { Episode } from "../../../models/resources/episode";
import { Show } from "../../../models/resources/show";
import { Episode } from "../../models/resources/episode";
import { Show } from "../../models/resources/show";
import {MatDialog} from "@angular/material/dialog";
import {TrailerDialogComponent} from "../trailer-dialog/trailer-dialog.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 {Page} from "../../../models/page";
import {People} from "../../../models/resources/people";
import {Page} from "../../models/page";
import {People} from "../../models/resources/people";
@Component({
selector: 'app-show-details',

View File

@ -1,15 +1,16 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs"
import {Page} from "../../models/page";
import {IResource} from "../../models/resources/resource";
import {Library} from "../../models/resources/library";
import {LibraryItem} from "../../models/resources/library-item";
import {Page} from "../models/page";
import {IResource} from "../models/resources/resource";
import {Library} from "../models/resources/library";
import {LibraryItem} from "../models/resources/library-item";
import {map} from "rxjs/operators";
import {Season} from "../../models/resources/season";
import {Episode} from "../../models/resources/episode";
import {People} from "../../models/resources/people";
import {Show} from "../../models/resources/show";
import {Season} from "../models/resources/season";
import {Episode} from "../models/resources/episode";
import {People} from "../models/resources/people";
import {Show} from "../models/resources/show";
import { Studio } from "../models/resources/studio";
export interface ApiArgs
{
@ -57,6 +58,11 @@ class CrudApi<T extends IResource>
{
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({
@ -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 {Observable, EMPTY} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
import {Page} from "../../models/page";
import {IResource} from "../../models/resources/resource";
import {Page} from "../models/page";
import {IResource} from "../models/resources/resource";
type RouteMapper = (route: ActivatedRouteSnapshot, endpoint: string, queryParams: [string, string][]) => string;

View File

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

View File

@ -1,5 +1,5 @@
import { detect } from "detect-browser";
import { Track, WatchItem } from "../models/watch-item";
import { Track, WatchItem } from "../app/models/watch-item";
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>