mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05:00 
			
		
		
		
	Removing the submodule
This commit is contained in:
		
							parent
							
								
									3f224636c7
								
							
						
					
					
						commit
						d85090d3ef
					
				@ -1,14 +0,0 @@
 | 
			
		||||
# Editor configuration, see https://editorconfig.org
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
indent_style = tab
 | 
			
		||||
indent_size = 4
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
max_line_length = 120
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
 | 
			
		||||
[*.md]
 | 
			
		||||
max_line_length = off
 | 
			
		||||
trim_trailing_whitespace = false
 | 
			
		||||
							
								
								
									
										46
									
								
								Kyoo.WebApp/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								Kyoo.WebApp/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,46 +0,0 @@
 | 
			
		||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# compiled output
 | 
			
		||||
/dist
 | 
			
		||||
/tmp
 | 
			
		||||
/out-tsc
 | 
			
		||||
# Only exists if Bazel was run
 | 
			
		||||
/bazel-out
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
 | 
			
		||||
# profiling files
 | 
			
		||||
chrome-profiler-events*.json
 | 
			
		||||
speed-measure-plugin*.json
 | 
			
		||||
 | 
			
		||||
# IDEs and editors
 | 
			
		||||
/.idea
 | 
			
		||||
.project
 | 
			
		||||
.classpath
 | 
			
		||||
.c9/
 | 
			
		||||
*.launch
 | 
			
		||||
.settings/
 | 
			
		||||
*.sublime-workspace
 | 
			
		||||
 | 
			
		||||
# IDE - VSCode
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/settings.json
 | 
			
		||||
!.vscode/tasks.json
 | 
			
		||||
!.vscode/launch.json
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.history/*
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
/.sass-cache
 | 
			
		||||
/connect.lock
 | 
			
		||||
/coverage
 | 
			
		||||
/libpeerconnection.log
 | 
			
		||||
npm-debug.log
 | 
			
		||||
yarn-error.log
 | 
			
		||||
testem.log
 | 
			
		||||
/typings
 | 
			
		||||
 | 
			
		||||
# System Files
 | 
			
		||||
.DS_Store
 | 
			
		||||
Thumbs.db
 | 
			
		||||
@ -1,2 +0,0 @@
 | 
			
		||||
# Kyoo.WebApp
 | 
			
		||||
The Angular web app for Kyoo.
 | 
			
		||||
@ -1,112 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
 | 
			
		||||
    "version": 1,
 | 
			
		||||
    "newProjectRoot": "projects",
 | 
			
		||||
    "projects": {
 | 
			
		||||
        "kyoo": {
 | 
			
		||||
            "projectType": "application",
 | 
			
		||||
            "schematics": {
 | 
			
		||||
                "@schematics/angular:component": {
 | 
			
		||||
                    "style": "scss"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "root": "",
 | 
			
		||||
            "sourceRoot": "src",
 | 
			
		||||
            "prefix": "app",
 | 
			
		||||
            "architect": {
 | 
			
		||||
                "build": {
 | 
			
		||||
                    "builder": "@angular-devkit/build-angular:browser",
 | 
			
		||||
                    "options": {
 | 
			
		||||
                        "outputPath": "dist",
 | 
			
		||||
                        "index": "src/index.html",
 | 
			
		||||
                        "main": "src/main.ts",
 | 
			
		||||
                        "polyfills": "src/polyfills.ts",
 | 
			
		||||
                        "tsConfig": "tsconfig.json",
 | 
			
		||||
                        "aot": false,
 | 
			
		||||
                        "assets": [
 | 
			
		||||
                            "src/assets",
 | 
			
		||||
                            {
 | 
			
		||||
                                "input": "node_modules/libass-wasm/dist/js",
 | 
			
		||||
                                "glob": "subtitles-octopus-worker*",
 | 
			
		||||
                                "output": "."
 | 
			
		||||
                            }
 | 
			
		||||
                        ],
 | 
			
		||||
                        "styles": [
 | 
			
		||||
                            "src/styles.scss"
 | 
			
		||||
                        ],
 | 
			
		||||
                        "scripts": [
 | 
			
		||||
                            "./node_modules/jquery/dist/jquery.min.js",
 | 
			
		||||
                            "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
 | 
			
		||||
                            "./node_modules/hls.js/dist/hls.js"
 | 
			
		||||
                        ],
 | 
			
		||||
                        "stylePreprocessorOptions": {
 | 
			
		||||
                            "includePaths": ["src"]
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "configurations": {
 | 
			
		||||
                        "production": {
 | 
			
		||||
                            "fileReplacements": [
 | 
			
		||||
                                {
 | 
			
		||||
                                    "replace": "src/environments/environment.ts",
 | 
			
		||||
                                    "with": "src/environments/environment.prod.ts"
 | 
			
		||||
                                }
 | 
			
		||||
                            ],
 | 
			
		||||
                            "optimization": true,
 | 
			
		||||
                            "outputHashing": "all",
 | 
			
		||||
                            "sourceMap": false,
 | 
			
		||||
                            "extractCss": true,
 | 
			
		||||
                            "namedChunks": false,
 | 
			
		||||
                            "aot": true,
 | 
			
		||||
                            "extractLicenses": true,
 | 
			
		||||
                            "vendorChunk": false,
 | 
			
		||||
                            "buildOptimizer": true,
 | 
			
		||||
                            "budgets": [
 | 
			
		||||
                                {
 | 
			
		||||
                                    "type": "initial",
 | 
			
		||||
                                    "maximumWarning": "3mb",
 | 
			
		||||
                                    "maximumError": "5mb"
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    "type": "anyComponentStyle",
 | 
			
		||||
                                    "maximumWarning": "6kb",
 | 
			
		||||
                                    "maximumError": "10kb"
 | 
			
		||||
                                }
 | 
			
		||||
                            ]
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "serve": {
 | 
			
		||||
                    "builder": "@angular-devkit/build-angular:dev-server",
 | 
			
		||||
                    "options": {
 | 
			
		||||
                        "browserTarget": "kyoo:build"
 | 
			
		||||
                    },
 | 
			
		||||
                    "configurations": {
 | 
			
		||||
                        "production": {
 | 
			
		||||
                            "browserTarget": "kyoo:build:production"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "extract-i18n": {
 | 
			
		||||
                    "builder": "@angular-devkit/build-angular:extract-i18n",
 | 
			
		||||
                    "options": {
 | 
			
		||||
                        "browserTarget": "kyoo:build"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "lint": {
 | 
			
		||||
                    "builder": "@angular-devkit/build-angular:tslint",
 | 
			
		||||
                    "options": {
 | 
			
		||||
                        "tsConfig": "tsconfig.json",
 | 
			
		||||
                        "exclude": [
 | 
			
		||||
                            "**/node_modules/**",
 | 
			
		||||
			    "dist"
 | 
			
		||||
                        ]
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "defaultProject": "kyoo",
 | 
			
		||||
    "cli": {
 | 
			
		||||
        "analytics": false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29718
									
								
								Kyoo.WebApp/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										29718
									
								
								Kyoo.WebApp/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,56 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "kyoo",
 | 
			
		||||
    "version": "0.0.0",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "ng": "ng",
 | 
			
		||||
        "start": "ng serve",
 | 
			
		||||
        "build": "ng build",
 | 
			
		||||
        "lint": "ng lint",
 | 
			
		||||
        "postinstall": "./subtitles-octopus-fix.sh"
 | 
			
		||||
    },
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "browserslist": [
 | 
			
		||||
        "> 0.5%",
 | 
			
		||||
        "last 2 versions",
 | 
			
		||||
        "Firefox ESR",
 | 
			
		||||
        "not dead",
 | 
			
		||||
        "not IE 9-11"
 | 
			
		||||
    ],
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@angular/animations": "^11.2.6",
 | 
			
		||||
        "@angular/cdk": "^11.2.5",
 | 
			
		||||
        "@angular/common": "^11.2.6",
 | 
			
		||||
        "@angular/compiler": "^11.2.6",
 | 
			
		||||
        "@angular/core": "^11.2.6",
 | 
			
		||||
        "@angular/forms": "^11.2.6",
 | 
			
		||||
        "@angular/material": "^11.2.5",
 | 
			
		||||
        "@angular/platform-browser": "^11.2.6",
 | 
			
		||||
        "@angular/platform-browser-dynamic": "^11.2.6",
 | 
			
		||||
        "@angular/router": "^11.2.6",
 | 
			
		||||
        "angular-auth-oidc-client": "11.6.4",
 | 
			
		||||
        "bootstrap": "^4.6.0",
 | 
			
		||||
        "detect-browser": "^5.2.0",
 | 
			
		||||
        "hls.js": "^0.14.17",
 | 
			
		||||
        "jquery": "^3.6.0",
 | 
			
		||||
        "libass-wasm": "https://codeload.github.com/jellyfin/JavascriptSubtitlesOctopus/tar.gz/4.0.0-jf-smarttv",
 | 
			
		||||
        "ngx-infinite-scroll": "^10.0.1",
 | 
			
		||||
        "popper.js": "^1.16.1",
 | 
			
		||||
        "rxjs": "^6.6.6",
 | 
			
		||||
        "zone.js": "^0.11.4"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@angular-devkit/build-angular": "^0.1102.5",
 | 
			
		||||
        "@angular/cli": "^11.2.5",
 | 
			
		||||
        "@angular/compiler-cli": "^11.2.6",
 | 
			
		||||
        "@angular/language-service": "^11.2.6",
 | 
			
		||||
        "@types/bootstrap": "^5.0.9",
 | 
			
		||||
        "@types/hls.js": "^0.13.3",
 | 
			
		||||
        "@types/jquery": "^3.5.5",
 | 
			
		||||
        "@types/node": "^14.14.35",
 | 
			
		||||
        "@types/video.js": "^7.3.15",
 | 
			
		||||
        "codelyzer": "^6.0.1",
 | 
			
		||||
        "ts-node": "~9.1.1",
 | 
			
		||||
        "tslint": "^6.1.3",
 | 
			
		||||
        "typescript": "<4.2"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,117 +0,0 @@
 | 
			
		||||
import { NgModule } from "@angular/core";
 | 
			
		||||
import { RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
 | 
			
		||||
import { ItemsGridComponent } from "./components/items-grid/items-grid.component";
 | 
			
		||||
import { CustomRouteReuseStrategy } from "./misc/custom-route-reuse-strategy";
 | 
			
		||||
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 {
 | 
			
		||||
	EpisodeService,
 | 
			
		||||
	LibraryItemService,
 | 
			
		||||
	LibraryService,
 | 
			
		||||
	PeopleService,
 | 
			
		||||
	SeasonService,
 | 
			
		||||
	ShowService
 | 
			
		||||
} from "./services/api.service";
 | 
			
		||||
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 { SearchComponent } from "./pages/search/search.component";
 | 
			
		||||
import { SearchResult } from "./models/search-result";
 | 
			
		||||
import { PlayerComponent } from "./pages/player/player.component";
 | 
			
		||||
import { WatchItem } from "./models/watch-item";
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
	{path: "browse", component: ItemsGridComponent, pathMatch: "full",
 | 
			
		||||
		resolve: {items: PageResolver.forResource<LibraryItem>("items", ItemsGridComponent.routeMapper)},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		runGuardsAndResolvers: "always"
 | 
			
		||||
	},
 | 
			
		||||
	{path: "browse/:slug", component: ItemsGridComponent,
 | 
			
		||||
		resolve: {items: PageResolver.forResource<LibraryItem>("library/:slug/items", ItemsGridComponent.routeMapper)},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		runGuardsAndResolvers: "always",
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{path: "genre/:slug", component: ItemsGridComponent,
 | 
			
		||||
		resolve: {items: PageResolver.forResource<Show>("shows", ItemsGridComponent.routeMapper, "genres=ctn::slug")},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		runGuardsAndResolvers: "always"
 | 
			
		||||
	},
 | 
			
		||||
	{path: "studio/:slug", component: ItemsGridComponent,
 | 
			
		||||
		resolve: {items: PageResolver.forResource<Show>("shows", ItemsGridComponent.routeMapper, "studio=:slug")},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		runGuardsAndResolvers: "always"
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{path: "collection/:slug", component: CollectionComponent,
 | 
			
		||||
		resolve:
 | 
			
		||||
		{
 | 
			
		||||
			collection: ItemResolver.forResource<Collection>("collections/:slug"),
 | 
			
		||||
			shows: PageResolver.forResource<Show>("collections/:slug/shows", ItemsGridComponent.routeMapper)
 | 
			
		||||
		},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		runGuardsAndResolvers: "always"
 | 
			
		||||
	},
 | 
			
		||||
	{path: "people/:slug", component: CollectionComponent,
 | 
			
		||||
		resolve:
 | 
			
		||||
		{
 | 
			
		||||
			collection: ItemResolver.forResource<Collection>("people/:slug"),
 | 
			
		||||
			shows: PageResolver.forResource<Show>("people/:slug/roles", ItemsGridComponent.routeMapper)
 | 
			
		||||
		},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		runGuardsAndResolvers: "always"
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{path: "show/:slug", component: ShowDetailsComponent,
 | 
			
		||||
		resolve: {show: ItemResolver.forResource<Show>("shows/:slug?fields=studio,genres,seasons,externalIDs")},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")]
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{path: "search/:query", component: SearchComponent,
 | 
			
		||||
		resolve: {items: ItemResolver.forResource<SearchResult>("search/:query")},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("read")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("read")]
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{path: "watch/:item", component: PlayerComponent,
 | 
			
		||||
		resolve: {item: ItemResolver.forResource<WatchItem>("watch/:item")},
 | 
			
		||||
		canLoad: [AuthGuard.forPermissions("play")],
 | 
			
		||||
		canActivate: [AuthGuard.forPermissions("play")]
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// TODO implement an home page.
 | 
			
		||||
 | 
			
		||||
	{path: "", pathMatch: "full", redirectTo: "/browse"},
 | 
			
		||||
	{path: "**", component: NotFoundComponent}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
	imports: [RouterModule.forRoot(routes,
 | 
			
		||||
		{
 | 
			
		||||
			scrollPositionRestoration: "enabled"
 | 
			
		||||
		})],
 | 
			
		||||
	exports: [RouterModule],
 | 
			
		||||
	providers: [
 | 
			
		||||
		LibraryService,
 | 
			
		||||
		LibraryItemService,
 | 
			
		||||
		PeopleService,
 | 
			
		||||
		ShowService,
 | 
			
		||||
		SeasonService,
 | 
			
		||||
		EpisodeService,
 | 
			
		||||
		PageResolver.resolvers,
 | 
			
		||||
		ItemResolver.resolvers,
 | 
			
		||||
		{provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy}
 | 
			
		||||
	]
 | 
			
		||||
})
 | 
			
		||||
export class AppRoutingModule { }
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
<header id="nav">
 | 
			
		||||
	<div class="fixed-top">
 | 
			
		||||
		<nav id="toolbar" class="navbar navbar-dark bg-secondary flex-nowrap">
 | 
			
		||||
			<button mat-icon-button class="icon p-0 d-sm-none" type="button" data-toggle="collapse" data-target=".mobile-nav">
 | 
			
		||||
				<mat-icon>menu</mat-icon>
 | 
			
		||||
			</button>
 | 
			
		||||
			<a class="navbar-brand nav-item ml-3" routerLink="/">
 | 
			
		||||
				Kyoo
 | 
			
		||||
			</a>
 | 
			
		||||
 | 
			
		||||
			<ul class="navbar-nav flex-row d-none d-sm-flex">
 | 
			
		||||
				<ng-container *ngTemplateOutlet="links"></ng-container>
 | 
			
		||||
			</ul>
 | 
			
		||||
 | 
			
		||||
			<ul class="navbar-nav flex-row flex-nowrap ml-auto">
 | 
			
		||||
				<li class="nav-item icon searchbar">
 | 
			
		||||
					<mat-icon matTooltipPosition="below" matTooltip="Search" (click)="openSearch()">search</mat-icon>
 | 
			
		||||
					<input placeholder="Search" id="search" type="search" (input)="onUpdateValue($any($event))"/>
 | 
			
		||||
				</li>
 | 
			
		||||
 | 
			
		||||
				<li class="nav-item" *ngIf="!this.isAuthenticated else accountDrop">
 | 
			
		||||
					<a class="icon" (click)="this.authManager.login()" matTooltipPosition="below" matTooltip="Login">
 | 
			
		||||
						<mat-icon>account_circle</mat-icon>
 | 
			
		||||
					</a>
 | 
			
		||||
				</li>
 | 
			
		||||
				<ng-template #accountDrop>
 | 
			
		||||
					<li #accountParent class="nav-item icon" style="opacity: 1 !important;">
 | 
			
		||||
						<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>
 | 
			
		||||
						<button class="dropButton" mat-menu-item (click)="this.authManager.logout()">Logout</button>
 | 
			
		||||
					</mat-menu>
 | 
			
		||||
				</ng-template>
 | 
			
		||||
			</ul>
 | 
			
		||||
		</nav>
 | 
			
		||||
 | 
			
		||||
		<div class="d-sm-none navbar-dark bg-secondary">
 | 
			
		||||
			<ul class="mobile-nav collapse navbar-nav">
 | 
			
		||||
				<ng-container *ngTemplateOutlet="links"></ng-container>
 | 
			
		||||
			</ul>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<mat-progress-bar *ngIf="this.isLoading" color="accent" mode="indeterminate"> </mat-progress-bar>
 | 
			
		||||
	</div>
 | 
			
		||||
</header>
 | 
			
		||||
 | 
			
		||||
<main id="main">
 | 
			
		||||
	<router-outlet></router-outlet>
 | 
			
		||||
</main>
 | 
			
		||||
 | 
			
		||||
<ng-template #links>
 | 
			
		||||
	<li class="nav-item">
 | 
			
		||||
		<a class="nav-link" routerLink="/browse" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">All</a>
 | 
			
		||||
	</li>
 | 
			
		||||
	<li class="nav-item" *ngFor="let library of this.libraries">
 | 
			
		||||
		<a class="nav-link" routerLink="/browse/{{library.slug}}" routerLinkActive="active">{{library.name}}</a>
 | 
			
		||||
	</li>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -1,161 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
@import "variables";
 | 
			
		||||
 | 
			
		||||
#toolbar
 | 
			
		||||
{
 | 
			
		||||
	height: $nav-bar-height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar
 | 
			
		||||
{
 | 
			
		||||
	justify-content: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-item
 | 
			
		||||
{
 | 
			
		||||
	outline: none;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-down(sm)
 | 
			
		||||
	{
 | 
			
		||||
		text-align: center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> a
 | 
			
		||||
	{
 | 
			
		||||
		outline: none;
 | 
			
		||||
		color: inherit;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link
 | 
			
		||||
{
 | 
			
		||||
	outline: none;
 | 
			
		||||
	color: inherit;
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
	{
 | 
			
		||||
		text-decoration: none !important;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link
 | 
			
		||||
{
 | 
			
		||||
	padding: 12px;
 | 
			
		||||
	color: rgba(255, 255, 255, 0.7) !important;
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover, &:focus
 | 
			
		||||
	{
 | 
			
		||||
		color: white !important;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.active
 | 
			
		||||
	{
 | 
			
		||||
		color: var(--accentColor) !important;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-brand
 | 
			
		||||
{
 | 
			
		||||
	&:hover
 | 
			
		||||
	{
 | 
			
		||||
		color: var(--accentColor);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (max-width: 350px)
 | 
			
		||||
	{
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.searchbar
 | 
			
		||||
{
 | 
			
		||||
	border-radius: 30px;
 | 
			
		||||
	display: flex !important;
 | 
			
		||||
	flex-flow: row-reverse nowrap;
 | 
			
		||||
 | 
			
		||||
	> input
 | 
			
		||||
	{
 | 
			
		||||
		background: none !important;
 | 
			
		||||
		color: white;
 | 
			
		||||
		outline: none;
 | 
			
		||||
		border: none;
 | 
			
		||||
		border-bottom: 1px solid #cfcfcf;
 | 
			
		||||
		width: 0;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		max-width: 20rem;
 | 
			
		||||
		transition: width 0.4s ease-in-out;
 | 
			
		||||
 | 
			
		||||
		&:focus, &.searching
 | 
			
		||||
		{
 | 
			
		||||
			width: 100%;
 | 
			
		||||
 | 
			
		||||
			@include media-breakpoint-up(md)
 | 
			
		||||
			{
 | 
			
		||||
				width: 20rem;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input::-webkit-search-cancel-button
 | 
			
		||||
{
 | 
			
		||||
	display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon
 | 
			
		||||
{
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	opacity: 0.7;
 | 
			
		||||
	outline: none;
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover, &:focus
 | 
			
		||||
	{
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.profilePicture
 | 
			
		||||
{
 | 
			
		||||
	width: 24px;
 | 
			
		||||
	height: 24px;
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	vertical-align: middle;
 | 
			
		||||
	border-radius: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropButton
 | 
			
		||||
{
 | 
			
		||||
	outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main
 | 
			
		||||
{
 | 
			
		||||
	margin-top: $nav-bar-height;
 | 
			
		||||
	padding-top: 4px;
 | 
			
		||||
	max-height: calc(100vh - #{$nav-bar-height});
 | 
			
		||||
	display: block;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
	scrollbar-color: #999 transparent;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar
 | 
			
		||||
	{
 | 
			
		||||
		width: 8px;
 | 
			
		||||
		background: transparent;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar-thumb
 | 
			
		||||
	{
 | 
			
		||||
		background-color: #999;
 | 
			
		||||
 | 
			
		||||
		&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
		{
 | 
			
		||||
			background-color: rgb(134, 127, 127);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,100 +0,0 @@
 | 
			
		||||
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 { AccountComponent } from "./auth/account/account.component";
 | 
			
		||||
import { AuthService } from "./auth/auth.service";
 | 
			
		||||
import { Library } from "./models/resources/library";
 | 
			
		||||
import { LibraryService } from "./services/api.service";
 | 
			
		||||
// noinspection ES6UnusedImports
 | 
			
		||||
import * as $ from "jquery";
 | 
			
		||||
import ChangeEvent = JQuery.ChangeEvent;
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-root",
 | 
			
		||||
	templateUrl: "./app.component.html",
 | 
			
		||||
	styleUrls: ["./app.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class AppComponent
 | 
			
		||||
{
 | 
			
		||||
	static isMobile: boolean = false;
 | 
			
		||||
	libraries: Library[];
 | 
			
		||||
	isLoading: boolean = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	constructor(private libraryService: LibraryService,
 | 
			
		||||
	            private router: Router,
 | 
			
		||||
	            private location: Location,
 | 
			
		||||
	            public authManager: AuthService,
 | 
			
		||||
	            public dialog: MatDialog)
 | 
			
		||||
	{
 | 
			
		||||
		libraryService.getAll().subscribe(result =>
 | 
			
		||||
		{
 | 
			
		||||
			this.libraries = result.items;
 | 
			
		||||
		}, error => console.error(error));
 | 
			
		||||
 | 
			
		||||
		this.router.events.subscribe((event: Event) =>
 | 
			
		||||
		{
 | 
			
		||||
			switch (true)
 | 
			
		||||
			{
 | 
			
		||||
				case event instanceof NavigationStart:
 | 
			
		||||
					this.isLoading = true;
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				case event instanceof NavigationEnd:
 | 
			
		||||
				case event instanceof NavigationCancel:
 | 
			
		||||
				case event instanceof NavigationError:
 | 
			
		||||
					this.isLoading = false;
 | 
			
		||||
					break;
 | 
			
		||||
				default:
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		AppComponent.isMobile = !!navigator.userAgent.match(/Mobi/);
 | 
			
		||||
		if (!AppComponent.isMobile)
 | 
			
		||||
			document.body.classList.add("hoverEnabled");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get isAuthenticated(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return this.authManager.isAuthenticated;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	openSearch(): void
 | 
			
		||||
	{
 | 
			
		||||
		const input: HTMLInputElement = document.getElementById("search") as HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
		input.value = "";
 | 
			
		||||
		input.focus();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onUpdateValue(event: ChangeEvent<HTMLInputElement>): void
 | 
			
		||||
	{
 | 
			
		||||
		const query: string = event.target.value;
 | 
			
		||||
		if (query !== "")
 | 
			
		||||
		{
 | 
			
		||||
			event.target.classList.add("searching");
 | 
			
		||||
			this.router.navigate(["/search", query], {
 | 
			
		||||
				replaceUrl: this.router.url.startsWith("/search")
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			event.target.classList.remove("searching");
 | 
			
		||||
			this.location.back();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	openAccountDialog(): void
 | 
			
		||||
	{
 | 
			
		||||
		this.dialog.open(AccountComponent, {width: "500px", data: this.authManager.account});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,121 +0,0 @@
 | 
			
		||||
import { HttpClientModule } from "@angular/common/http";
 | 
			
		||||
import { APP_INITIALIZER, NgModule } from "@angular/core";
 | 
			
		||||
import { MatButtonModule } from "@angular/material/button";
 | 
			
		||||
import { MatCardModule } from "@angular/material/card";
 | 
			
		||||
import { MatRippleModule } from "@angular/material/core";
 | 
			
		||||
import { MatIconModule } from "@angular/material/icon";
 | 
			
		||||
import { MatMenuModule } from "@angular/material/menu";
 | 
			
		||||
import { MatProgressBarModule } from "@angular/material/progress-bar";
 | 
			
		||||
import { MatSelectModule } from "@angular/material/select";
 | 
			
		||||
import { MatSliderModule } from "@angular/material/slider";
 | 
			
		||||
import { MatSnackBarModule } from "@angular/material/snack-bar";
 | 
			
		||||
import { MatTooltipModule } from "@angular/material/tooltip";
 | 
			
		||||
import { BrowserModule, HammerModule } from "@angular/platform-browser";
 | 
			
		||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
 | 
			
		||||
import { AppRoutingModule } from "./app-routing.module";
 | 
			
		||||
import { AppComponent } from "./app.component";
 | 
			
		||||
import { ItemsGridComponent } from "./components/items-grid/items-grid.component";
 | 
			
		||||
import { CollectionComponent } from "./pages/collection/collection.component";
 | 
			
		||||
import { EpisodesListComponent } from "./components/episodes-list/episodes-list.component";
 | 
			
		||||
import { NotFoundComponent } from "./pages/not-found/not-found.component";
 | 
			
		||||
import { PeopleListComponent } from "./components/people-list/people-list.component";
 | 
			
		||||
import {
 | 
			
		||||
	BufferToWidthPipe,
 | 
			
		||||
	FormatTimePipe,
 | 
			
		||||
	PlayerComponent, SupportedButtonPipe,
 | 
			
		||||
	VolumeToButtonPipe
 | 
			
		||||
} from "./pages/player/player.component";
 | 
			
		||||
import { SearchComponent } from "./pages/search/search.component";
 | 
			
		||||
import { ShowDetailsComponent } from "./pages/show-details/show-details.component";
 | 
			
		||||
import { FormsModule , ReactiveFormsModule } from "@angular/forms";
 | 
			
		||||
import { MatInputModule } from "@angular/material/input";
 | 
			
		||||
import { MatFormFieldModule } from "@angular/material/form-field";
 | 
			
		||||
import { MatTabsModule } from "@angular/material/tabs";
 | 
			
		||||
import { PasswordValidator } from "./misc/password-validator";
 | 
			
		||||
import { MatCheckboxModule } from "@angular/material/checkbox";
 | 
			
		||||
import { MatDialogModule } from "@angular/material/dialog";
 | 
			
		||||
import { FallbackDirective } from "./misc/fallback.directive";
 | 
			
		||||
import { AuthModule } from "./auth/auth.module";
 | 
			
		||||
import { AuthRoutingModule } from "./auth/auth-routing.module";
 | 
			
		||||
import { TrailerDialogComponent } from "./pages/trailer-dialog/trailer-dialog.component";
 | 
			
		||||
import { ItemsListComponent } from "./components/items-list/items-list.component";
 | 
			
		||||
import { MetadataEditComponent } from "./pages/metadata-edit/metadata-edit.component";
 | 
			
		||||
import { MatChipsModule } from "@angular/material/chips";
 | 
			
		||||
import { MatAutocompleteModule } from "@angular/material/autocomplete";
 | 
			
		||||
import { MatExpansionModule } from "@angular/material/expansion";
 | 
			
		||||
import { InfiniteScrollModule } from "ngx-infinite-scroll";
 | 
			
		||||
import { ShowGridComponent } from "./components/show-grid/show-grid.component";
 | 
			
		||||
import { MatBadgeModule } from "@angular/material/badge";
 | 
			
		||||
import { StartupService } from "./services/startup.service";
 | 
			
		||||
import { LongPressDirective } from "./misc/long-press.directive";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
	declarations: [
 | 
			
		||||
		AppComponent,
 | 
			
		||||
		NotFoundComponent,
 | 
			
		||||
		ItemsGridComponent,
 | 
			
		||||
		ShowDetailsComponent,
 | 
			
		||||
		EpisodesListComponent,
 | 
			
		||||
		PlayerComponent,
 | 
			
		||||
		CollectionComponent,
 | 
			
		||||
		SearchComponent,
 | 
			
		||||
		PeopleListComponent,
 | 
			
		||||
		PasswordValidator,
 | 
			
		||||
		FallbackDirective,
 | 
			
		||||
		TrailerDialogComponent,
 | 
			
		||||
		ItemsListComponent,
 | 
			
		||||
		MetadataEditComponent,
 | 
			
		||||
		ShowGridComponent,
 | 
			
		||||
		FormatTimePipe,
 | 
			
		||||
		BufferToWidthPipe,
 | 
			
		||||
		VolumeToButtonPipe,
 | 
			
		||||
		SupportedButtonPipe,
 | 
			
		||||
		LongPressDirective
 | 
			
		||||
	],
 | 
			
		||||
	imports: [
 | 
			
		||||
		BrowserModule,
 | 
			
		||||
		HttpClientModule,
 | 
			
		||||
		AuthRoutingModule,
 | 
			
		||||
		AppRoutingModule,
 | 
			
		||||
		BrowserAnimationsModule,
 | 
			
		||||
		MatSnackBarModule,
 | 
			
		||||
		MatProgressBarModule,
 | 
			
		||||
		MatButtonModule,
 | 
			
		||||
		MatIconModule,
 | 
			
		||||
		MatSelectModule,
 | 
			
		||||
		MatMenuModule,
 | 
			
		||||
		MatSliderModule,
 | 
			
		||||
		MatTooltipModule,
 | 
			
		||||
		MatRippleModule,
 | 
			
		||||
		MatCardModule,
 | 
			
		||||
		ReactiveFormsModule,
 | 
			
		||||
		MatInputModule,
 | 
			
		||||
		MatFormFieldModule,
 | 
			
		||||
		MatDialogModule,
 | 
			
		||||
		FormsModule,
 | 
			
		||||
		MatTabsModule,
 | 
			
		||||
		MatCheckboxModule,
 | 
			
		||||
		AuthModule,
 | 
			
		||||
		MatChipsModule,
 | 
			
		||||
		MatAutocompleteModule,
 | 
			
		||||
		MatExpansionModule,
 | 
			
		||||
		InfiniteScrollModule,
 | 
			
		||||
		MatBadgeModule,
 | 
			
		||||
		HammerModule
 | 
			
		||||
	],
 | 
			
		||||
	bootstrap: [AppComponent],
 | 
			
		||||
	exports: [
 | 
			
		||||
		FallbackDirective
 | 
			
		||||
	],
 | 
			
		||||
	providers: [
 | 
			
		||||
		StartupService,
 | 
			
		||||
		{
 | 
			
		||||
			provide: APP_INITIALIZER,
 | 
			
		||||
			useFactory: (startup: StartupService) => () => startup.load(),
 | 
			
		||||
			deps: [StartupService],
 | 
			
		||||
			multi: true
 | 
			
		||||
		}
 | 
			
		||||
	]
 | 
			
		||||
})
 | 
			
		||||
export class AppModule { }
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
<h1 mat-dialog-title>Account</h1>
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<div class="col-8">
 | 
			
		||||
		<mat-form-field class="w-75">
 | 
			
		||||
			<mat-label>Email</mat-label>
 | 
			
		||||
			<input matInput name="accountEmail" [(ngModel)]="account.email" required email>
 | 
			
		||||
		</mat-form-field>
 | 
			
		||||
		<br/>
 | 
			
		||||
		<mat-form-field class="w-75">
 | 
			
		||||
			<mat-label>Username</mat-label>
 | 
			
		||||
			<input matInput name="accountUsername" [(ngModel)]="account.username" required>
 | 
			
		||||
		</mat-form-field>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="col-4">
 | 
			
		||||
		<input type="file" class="d-none" (change)="onPictureSelected($event)" #fileInput/>
 | 
			
		||||
		<div class="w-100 profilePicture">
 | 
			
		||||
			<img [src]="account.picture" alt="Profile picture" fallback="account.svg" #accountImg/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<button mat-icon-button class="upload_picture" matTooltipPosition="above" matTooltip="Upload picture" (click)="fileInput.click()">
 | 
			
		||||
			<mat-icon>photo_camera</mat-icon>
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<div mat-dialog-actions fxFlexAlign="end" align="end" style="text-align: end">
 | 
			
		||||
	<button mat-button (click)="cancel()">Cancel</button>
 | 
			
		||||
	<button mat-button (click)="finish()" cdkFocusInitial>Ok</button>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,38 +0,0 @@
 | 
			
		||||
* 
 | 
			
		||||
{
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	outline: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
*:before, *:after 
 | 
			
		||||
{
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.upload_picture
 | 
			
		||||
{
 | 
			
		||||
	position: absolute; 
 | 
			
		||||
	bottom: 2%;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.profilePicture
 | 
			
		||||
{
 | 
			
		||||
	padding-top: 100%;
 | 
			
		||||
	height: 0;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	
 | 
			
		||||
	> img 
 | 
			
		||||
	{
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		right: 0;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
		border-radius: 50%;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,49 +0,0 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: "app-account",
 | 
			
		||||
  templateUrl: "./account.component.html",
 | 
			
		||||
  styleUrls: ["./account.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class AccountComponent
 | 
			
		||||
{
 | 
			
		||||
	selectedPicture: File;
 | 
			
		||||
	@ViewChild("accountImg") accountImg: ElementRef;
 | 
			
		||||
 | 
			
		||||
	constructor(public dialogRef: MatDialogRef<AccountComponent>,
 | 
			
		||||
	            @Inject(MAT_DIALOG_DATA) public account: Account,
 | 
			
		||||
	            private http: HttpClient) {}
 | 
			
		||||
 | 
			
		||||
	finish(): void
 | 
			
		||||
	{
 | 
			
		||||
		const data: FormData = new FormData();
 | 
			
		||||
		data.append("email", this.account.email);
 | 
			
		||||
		data.append("username", this.account.username);
 | 
			
		||||
		data.append("picture", this.selectedPicture);
 | 
			
		||||
 | 
			
		||||
		this.http.post("api/account/update", data).subscribe(() =>
 | 
			
		||||
		{
 | 
			
		||||
			this.dialogRef.close(this.account);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cancel(): void
 | 
			
		||||
	{
 | 
			
		||||
		this.dialogRef.close();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onPictureSelected(event: any): void
 | 
			
		||||
	{
 | 
			
		||||
		this.selectedPicture = event.target.files[0];
 | 
			
		||||
		const reader: FileReader = new FileReader();
 | 
			
		||||
		reader.onloadend = () =>
 | 
			
		||||
		{
 | 
			
		||||
			this.accountImg.nativeElement.src = reader.result;
 | 
			
		||||
		};
 | 
			
		||||
		reader.readAsDataURL(this.selectedPicture);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
import { NgModule } from "@angular/core";
 | 
			
		||||
import { RouterModule, Routes } from "@angular/router";
 | 
			
		||||
import { UnauthorizedComponent } from "./unauthorized/unauthorized.component";
 | 
			
		||||
import { LogoutComponent } from "./logout/logout.component";
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
	{path: "logout", component: LogoutComponent},
 | 
			
		||||
	{path: "unauthorized", component: UnauthorizedComponent},
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
	imports: [RouterModule.forChild(routes)],
 | 
			
		||||
	exports: [RouterModule]
 | 
			
		||||
})
 | 
			
		||||
export class AuthRoutingModule { }
 | 
			
		||||
@ -1,101 +0,0 @@
 | 
			
		||||
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): () => Promise<any>
 | 
			
		||||
{
 | 
			
		||||
	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.Warn
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
	declarations: [
 | 
			
		||||
		AuthPipe,
 | 
			
		||||
		AccountComponent,
 | 
			
		||||
		UnauthorizedComponent,
 | 
			
		||||
		LogoutComponent
 | 
			
		||||
	],
 | 
			
		||||
	imports: [
 | 
			
		||||
		CommonModule,
 | 
			
		||||
		MatButtonModule,
 | 
			
		||||
		MatIconModule,
 | 
			
		||||
		MatSelectModule,
 | 
			
		||||
		MatMenuModule,
 | 
			
		||||
		MatSliderModule,
 | 
			
		||||
		MatTooltipModule,
 | 
			
		||||
		MatRippleModule,
 | 
			
		||||
		MatCardModule,
 | 
			
		||||
		MatInputModule,
 | 
			
		||||
		MatFormFieldModule,
 | 
			
		||||
		MatDialogModule,
 | 
			
		||||
		FormsModule,
 | 
			
		||||
		MatTabsModule,
 | 
			
		||||
		MatCheckboxModule,
 | 
			
		||||
		OidcModule.forRoot(),
 | 
			
		||||
		RouterModule
 | 
			
		||||
	],
 | 
			
		||||
	entryComponents: [
 | 
			
		||||
		AccountComponent
 | 
			
		||||
	],
 | 
			
		||||
	providers: [
 | 
			
		||||
		OidcConfigService,
 | 
			
		||||
		{
 | 
			
		||||
			provide: APP_INITIALIZER,
 | 
			
		||||
			useFactory: loadConfig,
 | 
			
		||||
			deps: [OidcConfigService],
 | 
			
		||||
			multi: true
 | 
			
		||||
		},
 | 
			
		||||
		AuthGuard.guards,
 | 
			
		||||
		{
 | 
			
		||||
			provide: HTTP_INTERCEPTORS,
 | 
			
		||||
			useClass: AuthorizerInterceptor,
 | 
			
		||||
			multi: true
 | 
			
		||||
		}
 | 
			
		||||
	]
 | 
			
		||||
})
 | 
			
		||||
export class AuthModule
 | 
			
		||||
{
 | 
			
		||||
	constructor(http: HttpClient)
 | 
			
		||||
	{
 | 
			
		||||
		AuthGuard.permissionsObservable = http.get<string[]>("/api/account/default-permissions")
 | 
			
		||||
			.pipe(tap(x => AuthGuard.defaultPermissions = x));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,43 +0,0 @@
 | 
			
		||||
import { Injectable } from "@angular/core";
 | 
			
		||||
import { OidcSecurityService } from "angular-auth-oidc-client";
 | 
			
		||||
import { Account } from "../models/account";
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
	providedIn: "root"
 | 
			
		||||
})
 | 
			
		||||
export class AuthService
 | 
			
		||||
{
 | 
			
		||||
	isAuthenticated: boolean = false;
 | 
			
		||||
	account: Account = null;
 | 
			
		||||
 | 
			
		||||
	constructor(private oidcSecurityService: OidcSecurityService)
 | 
			
		||||
	{
 | 
			
		||||
		this.oidcSecurityService.checkAuth()
 | 
			
		||||
			.subscribe((auth: boolean) => this.isAuthenticated = auth);
 | 
			
		||||
		this.oidcSecurityService.userData$.subscribe(x =>
 | 
			
		||||
		{
 | 
			
		||||
			if (x == null)
 | 
			
		||||
				return;
 | 
			
		||||
			this.account = {
 | 
			
		||||
				email: x.email,
 | 
			
		||||
				username: x.username,
 | 
			
		||||
				picture: x.picture,
 | 
			
		||||
				permissions: x.permissions.split(",")
 | 
			
		||||
			};
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	login(): void
 | 
			
		||||
	{
 | 
			
		||||
		this.oidcSecurityService.authorize();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logout(): void
 | 
			
		||||
	{
 | 
			
		||||
		// this.http.get("api/account/logout").subscribe(() =>
 | 
			
		||||
		// {
 | 
			
		||||
			this.oidcSecurityService.logoff();
 | 
			
		||||
			// document.cookie = "Authenticated=false; expires=" + new Date(2147483647 * 1000).toUTCString();
 | 
			
		||||
		// });
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
	<h1>Successfully logged out.</h1>
 | 
			
		||||
	<p>Go back to the <a href="/" routerLink="/" class="text-white">main page</a></p>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
import { Component } from "@angular/core";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: "app-logout",
 | 
			
		||||
  templateUrl: "./logout.component.html",
 | 
			
		||||
  styleUrls: ["./logout.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class LogoutComponent {}
 | 
			
		||||
@ -1,29 +0,0 @@
 | 
			
		||||
import { Injector, Pipe, PipeTransform } from "@angular/core";
 | 
			
		||||
import { HttpClient, HttpHeaders } from "@angular/common/http";
 | 
			
		||||
import { OidcSecurityService } from "angular-auth-oidc-client";
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
	name: "auth"
 | 
			
		||||
})
 | 
			
		||||
export class AuthPipe implements PipeTransform
 | 
			
		||||
{
 | 
			
		||||
	private oidcSecurity: OidcSecurityService;
 | 
			
		||||
 | 
			
		||||
	constructor(private injector: Injector, private http: HttpClient) {}
 | 
			
		||||
 | 
			
		||||
	async transform(uri: string): Promise<string>
 | 
			
		||||
	{
 | 
			
		||||
		if (this.oidcSecurity === undefined)
 | 
			
		||||
			this.oidcSecurity = this.injector.get(OidcSecurityService);
 | 
			
		||||
		const token: string = this.oidcSecurity.getToken();
 | 
			
		||||
		if (!token)
 | 
			
		||||
			return uri;
 | 
			
		||||
		const headers: HttpHeaders = new HttpHeaders({Authorization: "Bearer " + token});
 | 
			
		||||
		const img: Blob = await this.http.get(uri, {headers, responseType: "blob"}).toPromise();
 | 
			
		||||
		const reader: FileReader = new FileReader();
 | 
			
		||||
		return new Promise((resolve) => {
 | 
			
		||||
			reader.onloadend = () => resolve(reader.result as string);
 | 
			
		||||
			reader.readAsDataURL(img);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
import { Injectable } from "@angular/core";
 | 
			
		||||
import { CanActivate, CanLoad, Router } from "@angular/router";
 | 
			
		||||
import { Observable } from "rxjs";
 | 
			
		||||
import { AuthService } from "../auth.service";
 | 
			
		||||
 | 
			
		||||
@Injectable({providedIn: "root"})
 | 
			
		||||
export class AuthGuard
 | 
			
		||||
{
 | 
			
		||||
	public static guards: any[] = [];
 | 
			
		||||
	public static defaultPermissions: string[];
 | 
			
		||||
	public static permissionsObservable: Observable<string[]>;
 | 
			
		||||
 | 
			
		||||
	static forPermissions(...permissions: string[]): any
 | 
			
		||||
	{
 | 
			
		||||
		@Injectable()
 | 
			
		||||
		class AuthenticatedGuard implements CanActivate, CanLoad
 | 
			
		||||
		{
 | 
			
		||||
			constructor(private router: Router, private authManager: AuthService)  {}
 | 
			
		||||
 | 
			
		||||
			async canActivate(): Promise<boolean>
 | 
			
		||||
			{
 | 
			
		||||
				if (!await this.checkPermissions())
 | 
			
		||||
				{
 | 
			
		||||
					await this.router.navigate(["/unauthorized"]);
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			async canLoad(): Promise<boolean>
 | 
			
		||||
			{
 | 
			
		||||
				if (!await this.checkPermissions())
 | 
			
		||||
				{
 | 
			
		||||
					await this.router.navigate(["/unauthorized"]);
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			async checkPermissions(): Promise<boolean>
 | 
			
		||||
			{
 | 
			
		||||
				if (this.authManager.isAuthenticated)
 | 
			
		||||
				{
 | 
			
		||||
					const perms: string[] = this.authManager.account.permissions;
 | 
			
		||||
					for (const perm of permissions) {
 | 
			
		||||
						if (!perms.includes(perm))
 | 
			
		||||
							return false;
 | 
			
		||||
					}
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					if (!AuthGuard.defaultPermissions)
 | 
			
		||||
						await AuthGuard.permissionsObservable.toPromise();
 | 
			
		||||
 | 
			
		||||
					for (const perm of permissions)
 | 
			
		||||
						if (!AuthGuard.defaultPermissions.includes(perm))
 | 
			
		||||
							return false;
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		AuthGuard.guards.push(AuthenticatedGuard);
 | 
			
		||||
		return AuthenticatedGuard;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
import { Injectable, Injector } from "@angular/core";
 | 
			
		||||
import {
 | 
			
		||||
	HttpRequest,
 | 
			
		||||
	HttpHandler,
 | 
			
		||||
	HttpEvent,
 | 
			
		||||
	HttpInterceptor
 | 
			
		||||
} from "@angular/common/http";
 | 
			
		||||
import { Observable } from "rxjs";
 | 
			
		||||
import { OidcSecurityService } from "angular-auth-oidc-client";
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AuthorizerInterceptor implements HttpInterceptor
 | 
			
		||||
{
 | 
			
		||||
	private oidcSecurity: OidcSecurityService;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	constructor(private injector: Injector) {}
 | 
			
		||||
 | 
			
		||||
	intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
 | 
			
		||||
	{
 | 
			
		||||
		if (this.oidcSecurity === undefined)
 | 
			
		||||
			this.oidcSecurity = this.injector.get(OidcSecurityService);
 | 
			
		||||
		const token: string = this.oidcSecurity.getToken();
 | 
			
		||||
		if (token)
 | 
			
		||||
			request = request.clone({setHeaders: {Authorization: "Bearer " + token}});
 | 
			
		||||
		return next.handle(request);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
	<h1>Unauthorized</h1>
 | 
			
		||||
	<p>You don't have enough permissions to view this page.
 | 
			
		||||
		<span *ngIf="!this.isLoggedIn()"><br/>Sign in and try again.</span>
 | 
			
		||||
	</p>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
import { Component } from "@angular/core";
 | 
			
		||||
import { AuthService } from "../auth.service";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
 	selector: "app-unauthorized",
 | 
			
		||||
 	templateUrl: "./unauthorized.component.html",
 | 
			
		||||
 	styleUrls: ["./unauthorized.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class UnauthorizedComponent
 | 
			
		||||
{
 | 
			
		||||
 	constructor(private authManager: AuthService) { }
 | 
			
		||||
 | 
			
		||||
 	isLoggedIn(): boolean
 | 
			
		||||
    {
 | 
			
		||||
    	return this.authManager.isAuthenticated;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,48 +0,0 @@
 | 
			
		||||
<div class="root">
 | 
			
		||||
	<div class="episodes" #scrollView
 | 
			
		||||
	     (scroll)="onScroll()" infinite-scroll (scrolled)="this.episodes?.loadNext(this.client)"
 | 
			
		||||
	     [horizontal]="true" [scrollWindow]="false">
 | 
			
		||||
		<a *ngFor="let episode of this.episodes?.items; index as i;" draggable="false"
 | 
			
		||||
		    routerLink="/watch/{{episode.slug}}" href="/watch/{{episode.slug}}"
 | 
			
		||||
		    appLongPress (longPressed)="this.openMenu(i)"
 | 
			
		||||
		    class="episode" #itemsDom>
 | 
			
		||||
			<button mat-icon-button class="moreBtn" tabindex="-1"
 | 
			
		||||
			        [style.display]="this.openedIndex === i ? 'block' : undefined"
 | 
			
		||||
			        [matMenuTriggerFor]="more" [matMenuTriggerData]="{episode: episode}"
 | 
			
		||||
			        (menuOpened)="this.openedIndex = i" (menuClosed)="this.openedIndex = undefined"
 | 
			
		||||
			        (click)="$event.stopImmediatePropagation(); $event.preventDefault();">
 | 
			
		||||
				<mat-icon>more_vert</mat-icon>
 | 
			
		||||
			</button>
 | 
			
		||||
			<div>
 | 
			
		||||
				<div matRipple class="img" [style.background-image]="sanitize(episode.thumb)">
 | 
			
		||||
					<button mat-icon-button class="playBtn" tabindex="-1">
 | 
			
		||||
						<mat-icon class="playIcon">play_circle_outline</mat-icon>
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
				<ng-container *ngIf="displayShowTitle; else noTitle;">
 | 
			
		||||
					<h6 *ngIf="episode.seasonNumber != 0; else elseBlock;" class="title">{{episode.showTitle}} - S{{episode.seasonNumber}}:E{{episode.episodeNumber}}</h6>
 | 
			
		||||
					<ng-template #elseBlock><h6 class="title">{{episode.showTitle}}</h6></ng-template>
 | 
			
		||||
					<p class="subtitle">{{episode.title}}</p>
 | 
			
		||||
				</ng-container>
 | 
			
		||||
				<ng-template #noTitle>
 | 
			
		||||
					<h6 *ngIf="episode.seasonNumber != 0; else elseBlock;" class="title">S{{episode.seasonNumber}}:E{{episode.episodeNumber}} - {{episode.title}}</h6>
 | 
			
		||||
					<ng-template #elseBlock><h6 class="title">{{episode.title}}</h6></ng-template>
 | 
			
		||||
					<p class="overview">{{episode.overview}}</p>
 | 
			
		||||
				</ng-template>
 | 
			
		||||
			</div>
 | 
			
		||||
		</a>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<mat-menu #more="matMenu">
 | 
			
		||||
		<ng-template matMenuContent let-episode="episode">
 | 
			
		||||
			<a [href]="'/video/' + episode.slug" download><button mat-menu-item>Download episode</button></a>
 | 
			
		||||
		</ng-template>
 | 
			
		||||
	</mat-menu>
 | 
			
		||||
 | 
			
		||||
	<button mat-raised-button color="accent" class="scrollBtn leftBtn d-none" #leftBtn (click)="scrollLeft()">
 | 
			
		||||
		<mat-icon>arrow_left</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
	<button mat-raised-button color="accent" class="scrollBtn rightBtn" #rightBtn (click)="scrollRight()">
 | 
			
		||||
		<mat-icon>arrow_right</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,210 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
 | 
			
		||||
.root
 | 
			
		||||
{
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
	{
 | 
			
		||||
		.scrollBtn
 | 
			
		||||
		{
 | 
			
		||||
			display: block;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.episodes
 | 
			
		||||
{
 | 
			
		||||
	display: flex;
 | 
			
		||||
	padding-left: 15px;
 | 
			
		||||
	padding-right: 15px;
 | 
			
		||||
	overflow-x: auto;
 | 
			
		||||
	min-width: 100%;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
	scrollbar-width: thin;
 | 
			
		||||
	scrollbar-color: #999 transparent;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar
 | 
			
		||||
	{
 | 
			
		||||
		height: 4px;
 | 
			
		||||
		background: transparent;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar-thumb
 | 
			
		||||
	{
 | 
			
		||||
		background-color: #999;
 | 
			
		||||
		border-radius: 90px;
 | 
			
		||||
 | 
			
		||||
		&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
		{
 | 
			
		||||
			background-color: rgb(134, 127, 127);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.episode
 | 
			
		||||
{
 | 
			
		||||
	visibility: visible;
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	margin: .25rem .25rem 1.25rem;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	width: 55%;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	color: inherit;
 | 
			
		||||
	text-decoration: inherit;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		width: 40%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 33%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		width: 28%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(xl)
 | 
			
		||||
	{
 | 
			
		||||
		width: 18%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	> .moreBtn
 | 
			
		||||
	{
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 2%;
 | 
			
		||||
		right: 2%;
 | 
			
		||||
		width: 36px;
 | 
			
		||||
		height: 36px;
 | 
			
		||||
		outline: none;
 | 
			
		||||
		display: none;
 | 
			
		||||
		z-index: 255
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		.img
 | 
			
		||||
		{
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			height: 0;
 | 
			
		||||
			padding-top: 56.25%;
 | 
			
		||||
			background-color: #333333;
 | 
			
		||||
			background-size: contain;
 | 
			
		||||
			position: relative;
 | 
			
		||||
 | 
			
		||||
			> .playBtn
 | 
			
		||||
			{
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				bottom: 0;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				right: 0;
 | 
			
		||||
				margin: auto;
 | 
			
		||||
				width: 64px;
 | 
			
		||||
				height: 64px;
 | 
			
		||||
				outline: none;
 | 
			
		||||
				display: none;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.title
 | 
			
		||||
		{
 | 
			
		||||
			padding-top: .2rem;
 | 
			
		||||
			font-weight: 600;
 | 
			
		||||
			margin-bottom: 0;
 | 
			
		||||
			display: -webkit-box;
 | 
			
		||||
			-webkit-line-clamp: 1;
 | 
			
		||||
			-webkit-box-orient: vertical;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.overview
 | 
			
		||||
		{
 | 
			
		||||
			font-weight: 300;
 | 
			
		||||
			display: -webkit-box;
 | 
			
		||||
			-webkit-line-clamp: 4;
 | 
			
		||||
			-webkit-box-orient: vertical;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
			margin-bottom: 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.subtitle
 | 
			
		||||
		{
 | 
			
		||||
			font-weight: 300;
 | 
			
		||||
			display: -webkit-box;
 | 
			
		||||
			-webkit-line-clamp: 1;
 | 
			
		||||
			-webkit-box-orient: vertical;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover, &:host-context(.hoverEnabled) &:focus
 | 
			
		||||
	{
 | 
			
		||||
		.moreBtn
 | 
			
		||||
		{
 | 
			
		||||
			display: block;
 | 
			
		||||
			-webkit-touch-callout: none;
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		> div
 | 
			
		||||
		{
 | 
			
		||||
			.img 
 | 
			
		||||
			{
 | 
			
		||||
				outline: solid var(--accentColor);
 | 
			
		||||
 | 
			
		||||
				.playBtn 
 | 
			
		||||
				{
 | 
			
		||||
					display: block;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.title
 | 
			
		||||
			{
 | 
			
		||||
				text-decoration: underline;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.playIcon
 | 
			
		||||
{
 | 
			
		||||
	font-size: 64px;
 | 
			
		||||
	width: 64px;
 | 
			
		||||
	height: 64px;
 | 
			
		||||
	line-height: 64px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrollBtn
 | 
			
		||||
{
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	min-width: 0;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 20%;
 | 
			
		||||
	bottom: 60%;
 | 
			
		||||
	display: none;
 | 
			
		||||
 | 
			
		||||
	&.leftBtn
 | 
			
		||||
	{
 | 
			
		||||
		left: 0;
 | 
			
		||||
		padding-left: 10px;
 | 
			
		||||
		padding-right: 2px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.rightBtn
 | 
			
		||||
	{
 | 
			
		||||
		right: 0;
 | 
			
		||||
		padding-right: 10px;
 | 
			
		||||
		padding-left: 2px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
import { Component, Input, QueryList, ViewChildren } from "@angular/core";
 | 
			
		||||
import { MatMenuTrigger } from "@angular/material/menu";
 | 
			
		||||
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
 | 
			
		||||
import { Episode } from "../../models/resources/episode";
 | 
			
		||||
import { HorizontalScroller } from "../../misc/horizontal-scroller";
 | 
			
		||||
import { Page } from "../../models/page";
 | 
			
		||||
import { HttpClient } from "@angular/common/http";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-episodes-list",
 | 
			
		||||
	templateUrl: "./episodes-list.component.html",
 | 
			
		||||
	styleUrls: ["./episodes-list.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class EpisodesListComponent extends HorizontalScroller
 | 
			
		||||
{
 | 
			
		||||
	@Input() displayShowTitle = false;
 | 
			
		||||
	@Input() episodes: Page<Episode>;
 | 
			
		||||
	@ViewChildren(MatMenuTrigger) menus: QueryList<MatMenuTrigger>;
 | 
			
		||||
	openedIndex: number = undefined;
 | 
			
		||||
 | 
			
		||||
	constructor(private sanitizer: DomSanitizer, public client: HttpClient)
 | 
			
		||||
	{
 | 
			
		||||
		super();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sanitize(url: string): SafeStyle
 | 
			
		||||
	{
 | 
			
		||||
		return this.sanitizer.bypassSecurityTrustStyle("url(" + url + ")");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	openMenu(index: number): void
 | 
			
		||||
	{
 | 
			
		||||
		const menu: MatMenuTrigger = this.menus.find((x, i) => i === index);
 | 
			
		||||
		menu.focus();
 | 
			
		||||
		menu.openMenu();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,100 +0,0 @@
 | 
			
		||||
<div class="container-fluid justify-content-center" *ngIf="this.sortEnabled">
 | 
			
		||||
	<button mat-icon-button matTooltipPosition="below" matTooltip="Filter" [matMenuTriggerFor]="filterMenu">
 | 
			
		||||
		<mat-icon [matBadge]="getFilterCount().toString()" [matBadgeHidden]="getFilterCount() == 0"
 | 
			
		||||
		          matBadgeColor="warn" matBadgeSize="small">
 | 
			
		||||
			filter_list
 | 
			
		||||
		</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
	<button mat-button matTooltipPosition="below" matTooltip="Sort" [matMenuTriggerFor]="sortMenu">
 | 
			
		||||
		<mat-icon>sort</mat-icon> Sort by {{this.sortType}}
 | 
			
		||||
		<i *ngIf="this.sortUp" class="material-icons arrow">arrow_upward</i>
 | 
			
		||||
		<i *ngIf="!this.sortUp" class="material-icons arrow">arrow_downward</i>
 | 
			
		||||
	</button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<mat-menu #filterMenu="matMenu" class="big-panel">
 | 
			
		||||
	<ng-container *ngIf="this.genres.length > 0">
 | 
			
		||||
		<h4><b>Genres</b></h4>
 | 
			
		||||
		<mat-chip-list>
 | 
			
		||||
			<!--suppress AngularInvalidExpressionResultType ('default' color is valid for mat-chip)-->
 | 
			
		||||
			<mat-chip *ngFor="let genre of this.genres"
 | 
			
		||||
			          [color]="this.filters.genres.includes(genre) ? 'accent' : 'default'" selected
 | 
			
		||||
			          (click)="this.addFilter('genres', genre)">
 | 
			
		||||
				{{genre.name}}
 | 
			
		||||
			</mat-chip>
 | 
			
		||||
		</mat-chip-list>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
 | 
			
		||||
	<br/>
 | 
			
		||||
 | 
			
		||||
	<ng-container>
 | 
			
		||||
		<mat-form-field class="w-100 px-3" (click)="$event.stopPropagation();">
 | 
			
		||||
			<mat-label>Studio</mat-label>
 | 
			
		||||
			<input type="text" matInput [formControl]="studioForm"
 | 
			
		||||
			       [matAutocomplete]="autoStudio"
 | 
			
		||||
			       [value]="this.nameGetter(this.filters.studio)">
 | 
			
		||||
			<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>
 | 
			
		||||
				<mat-option *ngFor="let studio of this.filteredStudios | async" [value]="studio">
 | 
			
		||||
					{{studio.name}}
 | 
			
		||||
				</mat-option>
 | 
			
		||||
			</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">
 | 
			
		||||
	<div *ngFor="let type of this.sortKeys">
 | 
			
		||||
		<button *ngIf="type != this.sortType; else elseBlock;" mat-menu-item (click)="sort(type, true)">
 | 
			
		||||
			Sort by {{type}}
 | 
			
		||||
		</button>
 | 
			
		||||
		<ng-template #elseBlock>
 | 
			
		||||
			<button mat-menu-item (click)="sort(type, !this.sortUp)">
 | 
			
		||||
				Sort by {{type}}
 | 
			
		||||
				<i *ngIf="!this.sortUp" class="material-icons arrow">arrow_upward</i>
 | 
			
		||||
				<i *ngIf="this.sortUp" class="material-icons arrow">arrow_downward</i>
 | 
			
		||||
			</button>
 | 
			
		||||
		</ng-template>
 | 
			
		||||
	</div>
 | 
			
		||||
</mat-menu>
 | 
			
		||||
 | 
			
		||||
<div class="container-fluid justify-content-center"
 | 
			
		||||
     infinite-scroll (scrolled)="this.page?.loadNext(this.client)" infiniteScrollContainer="#main" fromRoot="true">
 | 
			
		||||
	<a class="show" *ngFor="let item of this.page?.items" draggable="false"
 | 
			
		||||
	   [href]="getLink(item)" [routerLink]="getLink(item)">
 | 
			
		||||
		<div matRipple [style.background-image]="getPoster(item)"></div>
 | 
			
		||||
		<p class="title">{{item.title ? item.title : item.name}}</p>
 | 
			
		||||
		<p class="date">{{getDate(item)}}</p>
 | 
			
		||||
	</a>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,127 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
 | 
			
		||||
button
 | 
			
		||||
{
 | 
			
		||||
	outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.arrow
 | 
			
		||||
{
 | 
			
		||||
	font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-fluid
 | 
			
		||||
{
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.show
 | 
			
		||||
{
 | 
			
		||||
	width: 27%;
 | 
			
		||||
	min-width: 100px;
 | 
			
		||||
	max-width: 168px;
 | 
			
		||||
	list-style: none;
 | 
			
		||||
	margin: .5em;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	color: inherit;
 | 
			
		||||
	outline: none;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		width: 22%;
 | 
			
		||||
		min-width: 120px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 18%;
 | 
			
		||||
		margin: 1em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		width: 18%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(xl)
 | 
			
		||||
	{
 | 
			
		||||
		width: 15%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	&:focus, &:hover
 | 
			
		||||
	{
 | 
			
		||||
		> div
 | 
			
		||||
		{
 | 
			
		||||
			outline: solid var(--accentColor);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .title
 | 
			
		||||
		{
 | 
			
		||||
			text-decoration: underline;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		padding-top: 147.0588%;
 | 
			
		||||
		background-size: cover;
 | 
			
		||||
		background-color: #333333;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> p
 | 
			
		||||
	{
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		text-overflow: ellipsis;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
 | 
			
		||||
		&.date
 | 
			
		||||
		{
 | 
			
		||||
			opacity: 0.8;
 | 
			
		||||
			font-size: 0.8em;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
	{
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .big-panel
 | 
			
		||||
{
 | 
			
		||||
	width: 80vw !important;
 | 
			
		||||
	max-width: none !important;
 | 
			
		||||
	margin-left: -20vw;
 | 
			
		||||
	margin-right: -20vw;
 | 
			
		||||
	overflow-x: hidden;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		width: 70vw !important;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 50vw !important;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		text-align: center;
 | 
			
		||||
 | 
			
		||||
		> mat-chip-list > div
 | 
			
		||||
		{
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
			margin: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,306 +0,0 @@
 | 
			
		||||
import { Component, Input, OnInit } from "@angular/core";
 | 
			
		||||
import { FormControl } from "@angular/forms";
 | 
			
		||||
import { ActivatedRoute, ActivatedRouteSnapshot, Params, Router } from "@angular/router";
 | 
			
		||||
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
 | 
			
		||||
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 { 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 { catchError, filter, map, mergeAll } from "rxjs/operators";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-items-grid",
 | 
			
		||||
	templateUrl: "./items-grid.component.html",
 | 
			
		||||
	styleUrls: ["./items-grid.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class ItemsGridComponent implements OnInit
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
	constructor(private route: ActivatedRoute,
 | 
			
		||||
	            private sanitizer: DomSanitizer,
 | 
			
		||||
	            private loader: PreLoaderService,
 | 
			
		||||
	            private router: Router,
 | 
			
		||||
	            private studioApi: StudioService,
 | 
			
		||||
	            private peopleApi: PeopleService,
 | 
			
		||||
	            public client: HttpClient)
 | 
			
		||||
	{
 | 
			
		||||
		this.route.data.subscribe((data) =>
 | 
			
		||||
		{
 | 
			
		||||
			this.page = data.items;
 | 
			
		||||
		});
 | 
			
		||||
		this.route.queryParams.subscribe((data) =>
 | 
			
		||||
		{
 | 
			
		||||
			this.updateGenresFilterFromQuery(data);
 | 
			
		||||
			this.updateStudioFilterFromQuery(data);
 | 
			
		||||
			this.updatePeopleFilterFromQuery(data);
 | 
			
		||||
		});
 | 
			
		||||
		this.loader.load<Genre>("/api/genres?limit=0").subscribe(data =>
 | 
			
		||||
		{
 | 
			
		||||
			this.genres = data;
 | 
			
		||||
			this.updateGenresFilterFromQuery(this.route.snapshot.queryParams);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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[] = [];
 | 
			
		||||
		if (query.genres?.startsWith("ctn:"))
 | 
			
		||||
			selectedGenres = query.genres.substr(4).split(",");
 | 
			
		||||
		else if (query.genres != null)
 | 
			
		||||
			selectedGenres = query.genres.split(",");
 | 
			
		||||
		if (this.router.url.startsWith("/genre"))
 | 
			
		||||
			selectedGenres.push(this.route.snapshot.params.slug);
 | 
			
		||||
 | 
			
		||||
		this.filters.genres = this.genres.filter(x => selectedGenres.includes(x.slug));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updateStudioFilterFromQuery(query: Params): void
 | 
			
		||||
	{
 | 
			
		||||
		const slug: string = this.router.url.startsWith("/studio") ? this.route.snapshot.params.slug : query.studio;
 | 
			
		||||
 | 
			
		||||
		if (slug && this.filters.studio?.slug !== slug)
 | 
			
		||||
		{
 | 
			
		||||
			this.filters.studio = {id: 0, slug, name: slug};
 | 
			
		||||
			this.studioApi.get(slug).subscribe(x => this.filters.studio = x);
 | 
			
		||||
		}
 | 
			
		||||
		else if (!slug)
 | 
			
		||||
			this.filters.studio = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updatePeopleFilterFromQuery(query: Params): void
 | 
			
		||||
	{
 | 
			
		||||
		let slugs: string[] = [];
 | 
			
		||||
		if (query.people != null)
 | 
			
		||||
		{
 | 
			
		||||
			if (query.people.startsWith("ctn:"))
 | 
			
		||||
				slugs = query.people.substr(4).split(",");
 | 
			
		||||
			else
 | 
			
		||||
				slugs = query.people.split(",");
 | 
			
		||||
		}
 | 
			
		||||
		else if (this.route.snapshot.params.slug && this.router.url.startsWith("/people"))
 | 
			
		||||
			slugs = [this.route.snapshot.params.slug];
 | 
			
		||||
 | 
			
		||||
		this.filters.people = slugs.map(x => ({slug: x, name: x} as People));
 | 
			
		||||
		for (const slug of slugs)
 | 
			
		||||
		{
 | 
			
		||||
			this.peopleApi.get(slug).subscribe(x =>
 | 
			
		||||
			{
 | 
			
		||||
				const i: number = this.filters.people.findIndex(y => y.slug === slug);
 | 
			
		||||
				this.filters.people[i] = x;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ngOnInit(): void
 | 
			
		||||
	{
 | 
			
		||||
		this.filteredStudios = this.studioForm.valueChanges
 | 
			
		||||
			.pipe(
 | 
			
		||||
				filter(x => x),
 | 
			
		||||
				map(x => typeof x === "string" ? x : x.name),
 | 
			
		||||
				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 [];
 | 
			
		||||
				})
 | 
			
		||||
			);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shouldDisplayNoneStudio(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return this.studioForm.value === "" || typeof this.studioForm.value !== "string";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getFilterCount(): number
 | 
			
		||||
	{
 | 
			
		||||
		let count: number = this.filters.genres.length + this.filters.people.length;
 | 
			
		||||
		if (this.filters.studio != null)
 | 
			
		||||
			count++;
 | 
			
		||||
		return count;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addFilter(category: string, resource: IResource, isArray: boolean = true, toggle: boolean = false): void
 | 
			
		||||
	{
 | 
			
		||||
		if (isArray)
 | 
			
		||||
		{
 | 
			
		||||
			if (this.filters[category].includes(resource) || this.filters[category].some(x => x.slug === resource.slug))
 | 
			
		||||
				this.filters[category].splice(this.filters[category].indexOf(resource), 1);
 | 
			
		||||
			else
 | 
			
		||||
				this.filters[category].push(resource);
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			if (resource && (this.filters[category] === resource || this.filters[category]?.slug === resource.slug))
 | 
			
		||||
			{
 | 
			
		||||
				if (!toggle)
 | 
			
		||||
					return;
 | 
			
		||||
				this.filters[category] = null;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
				this.filters[category] = resource;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let param: string = null;
 | 
			
		||||
		if (isArray && this.filters[category].length > 0)
 | 
			
		||||
			param = `${this.filters[category].map(x => x.slug).join(",")}`;
 | 
			
		||||
		else if (!isArray && this.filters[category] != null)
 | 
			
		||||
			param = resource.slug;
 | 
			
		||||
 | 
			
		||||
		if (/\/browse($|\?)/.test(this.router.url)
 | 
			
		||||
			|| this.router.url.startsWith("/genre")
 | 
			
		||||
			|| this.router.url.startsWith("/studio")
 | 
			
		||||
			|| this.router.url.startsWith("/people"))
 | 
			
		||||
		{
 | 
			
		||||
			if (this.filters.genres.length === 1 && this.getFilterCount() === 1)
 | 
			
		||||
			{
 | 
			
		||||
				this.router.navigate(["genre", this.filters.genres[0].slug], {
 | 
			
		||||
					replaceUrl: true,
 | 
			
		||||
					queryParams: {sortBy: this.route.snapshot.queryParams.sortBy}
 | 
			
		||||
				});
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (this.filters.studio != null && this.getFilterCount() === 1)
 | 
			
		||||
			{
 | 
			
		||||
				this.router.navigate(["studio", this.filters.studio.slug], {
 | 
			
		||||
					replaceUrl: true,
 | 
			
		||||
					queryParams: {sortBy: this.route.snapshot.queryParams.sortBy}
 | 
			
		||||
				});
 | 
			
		||||
				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")
 | 
			
		||||
			{
 | 
			
		||||
				const params: {[key: string]: string} = {[category]: param};
 | 
			
		||||
				if (this.router.url.startsWith("/studio") && category !== "studio")
 | 
			
		||||
					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,
 | 
			
		||||
					replaceUrl: true,
 | 
			
		||||
					queryParamsHandling: "merge"
 | 
			
		||||
				});
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		this.router.navigate([], {
 | 
			
		||||
			relativeTo: this.route,
 | 
			
		||||
			queryParams: {[category]: param},
 | 
			
		||||
			replaceUrl: true,
 | 
			
		||||
			queryParamsHandling: "merge"
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nameGetter(obj: Studio): string
 | 
			
		||||
	{
 | 
			
		||||
		return obj?.name ?? "None";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getPoster(obj: LibraryItem | Show | ShowRole | Collection): SafeStyle
 | 
			
		||||
	{
 | 
			
		||||
		return this.sanitizer.bypassSecurityTrustStyle(`url(${obj.poster})`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getDate(item: LibraryItem | Show | ShowRole | Collection): string
 | 
			
		||||
	{
 | 
			
		||||
		return ItemsUtils.getDate(item);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getLink(item: LibraryItem | Show | ShowRole | Collection): string
 | 
			
		||||
	{
 | 
			
		||||
		return ItemsUtils.getLink(item);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sort(type: string, order: boolean): void
 | 
			
		||||
	{
 | 
			
		||||
		this.sortType = type;
 | 
			
		||||
		this.sortUp = order;
 | 
			
		||||
 | 
			
		||||
		const param: string = `${this.sortType.replace(/\s/g, "")}:${this.sortUp ? "asc" : "desc"}`;
 | 
			
		||||
		this.router.navigate([], {
 | 
			
		||||
			relativeTo: this.route,
 | 
			
		||||
			queryParams: { sortBy: param },
 | 
			
		||||
			replaceUrl: true,
 | 
			
		||||
			queryParamsHandling: "merge"
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
<div class="scroll-row mb-5">
 | 
			
		||||
	<div class="container" #scrollView (scroll)="onScroll()"
 | 
			
		||||
		 infinite-scroll (scrolled)="this.items?.loadNext(this.client)" [horizontal]="true" [scrollWindow]="false">
 | 
			
		||||
		<a class="item" *ngFor="let item of this.items?.items" draggable="false"
 | 
			
		||||
		   [routerLink]="getLink(item)" [href]="getLink(item)" #itemsDom>
 | 
			
		||||
			<div matRipple [style.background-image]="getPoster(item)"> </div>
 | 
			
		||||
			<p class="title">{{item.title ? item.title : item.name}}</p>
 | 
			
		||||
			<p class="date">{{getDate(item)}}</p>
 | 
			
		||||
		</a>
 | 
			
		||||
	</div>
 | 
			
		||||
	<button mat-raised-button color="accent" class="scrollBtn leftBtn d-none" #leftBtn (click)="scrollLeft()">
 | 
			
		||||
		<mat-icon>arrow_left</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
	<button mat-raised-button color="accent" class="scrollBtn rightBtn" #rightBtn (click)="scrollRight()">
 | 
			
		||||
		<mat-icon>arrow_right</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,150 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
 | 
			
		||||
.container
 | 
			
		||||
{
 | 
			
		||||
	display: flex;
 | 
			
		||||
	padding-left: 15px;
 | 
			
		||||
	padding-right: 15px;
 | 
			
		||||
	overflow-x: auto;
 | 
			
		||||
	min-width: 100%;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
	scrollbar-width: thin;
 | 
			
		||||
	scrollbar-color: #999 transparent;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar
 | 
			
		||||
	{
 | 
			
		||||
		height: 4px;
 | 
			
		||||
		background: transparent;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar-thumb
 | 
			
		||||
	{
 | 
			
		||||
		background-color: #999;
 | 
			
		||||
		border-radius: 90px;
 | 
			
		||||
 | 
			
		||||
		&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
		{
 | 
			
		||||
			background-color: rgb(134, 127, 127);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item
 | 
			
		||||
{
 | 
			
		||||
	width: 33%;
 | 
			
		||||
	min-width: 120px;
 | 
			
		||||
	max-width: 200px;
 | 
			
		||||
	list-style: none;
 | 
			
		||||
	padding: .5em;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	color: inherit;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	flex-grow: 0;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		width: 25%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 20%;
 | 
			
		||||
		padding: 1em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		width: 18%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(xl)
 | 
			
		||||
	{
 | 
			
		||||
		width: 15%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	&:focus, &:hover
 | 
			
		||||
	{
 | 
			
		||||
		> div
 | 
			
		||||
		{
 | 
			
		||||
			outline: solid var(--accentColor);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .title
 | 
			
		||||
		{
 | 
			
		||||
			text-decoration: underline;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		padding-top: 147.0588%;
 | 
			
		||||
		background-size: cover;
 | 
			
		||||
		background-color: #333333;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> p
 | 
			
		||||
	{
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		text-overflow: ellipsis;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
 | 
			
		||||
		&.date
 | 
			
		||||
		{
 | 
			
		||||
			opacity: 0.8;
 | 
			
		||||
			font-size: 0.8em;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
	{
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scroll-row
 | 
			
		||||
{
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
	{
 | 
			
		||||
		.scrollBtn
 | 
			
		||||
		{
 | 
			
		||||
			display: block;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrollBtn
 | 
			
		||||
{
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	min-width: 0;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 30%;
 | 
			
		||||
	bottom: 40%;
 | 
			
		||||
	display: none;
 | 
			
		||||
 | 
			
		||||
	&.leftBtn
 | 
			
		||||
	{
 | 
			
		||||
		left: 0;
 | 
			
		||||
		padding-left: 10px;
 | 
			
		||||
		padding-right: 2px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.rightBtn
 | 
			
		||||
	{
 | 
			
		||||
		right: 0;
 | 
			
		||||
		padding-right: 10px;
 | 
			
		||||
		padding-left: 2px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,42 +0,0 @@
 | 
			
		||||
import { Component, Input } from "@angular/core";
 | 
			
		||||
import { Collection } from "../../models/resources/collection";
 | 
			
		||||
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
 | 
			
		||||
import { HorizontalScroller } from "../../misc/horizontal-scroller";
 | 
			
		||||
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 { ItemsUtils } from "../../misc/items-utils";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-items-list",
 | 
			
		||||
	templateUrl: "./items-list.component.html",
 | 
			
		||||
	styleUrls: ["./items-list.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class ItemsListComponent extends HorizontalScroller
 | 
			
		||||
{
 | 
			
		||||
	@Input() items: Page<Collection | Show | LibraryItem | ShowRole>;
 | 
			
		||||
	@Input() type: string;
 | 
			
		||||
 | 
			
		||||
	constructor(private sanitizer: DomSanitizer, public client: HttpClient)
 | 
			
		||||
	{
 | 
			
		||||
		super();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getPoster(item: LibraryItem | Show | ShowRole | Collection): SafeUrl
 | 
			
		||||
	{
 | 
			
		||||
		return this.sanitizer.bypassSecurityTrustStyle(`url(${item.poster})`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getDate(item: LibraryItem | Show | ShowRole | Collection): string
 | 
			
		||||
	{
 | 
			
		||||
		return ItemsUtils.getDate(item);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getLink(item: LibraryItem | Show | ShowRole | Collection): string
 | 
			
		||||
	{
 | 
			
		||||
		if (this.type)
 | 
			
		||||
			return `/${this.type}/${item.slug}`;
 | 
			
		||||
		return ItemsUtils.getLink(item);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
<div class="scroll-row mb-5">
 | 
			
		||||
	<div class="people-container" #scrollView
 | 
			
		||||
	     (scroll)="onScroll()" infinite-scroll (scrolled)="this.people?.loadNext(this.client)"
 | 
			
		||||
	     [horizontal]="true" [scrollWindow]="false">
 | 
			
		||||
		<a class="people" *ngFor="let people of this.people?.items" draggable="false"
 | 
			
		||||
		   routerLink="/people/{{people.slug}}" href="/people/{{people.slug}}" #itemsDom>
 | 
			
		||||
			<div matRipple [style.background-image]="getPeopleIcon(people)"> </div>
 | 
			
		||||
			<h6 class="name">{{people.name}}</h6>
 | 
			
		||||
			<p class="role">{{people.role}}</p>
 | 
			
		||||
		</a>
 | 
			
		||||
	</div>
 | 
			
		||||
	<button mat-raised-button color="accent" class="scrollBtn leftBtn d-none" #leftBtn (click)="scrollLeft()">
 | 
			
		||||
		<mat-icon>arrow_left</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
	<button mat-raised-button color="accent" class="scrollBtn rightBtn" #rightBtn (click)="scrollRight()">
 | 
			
		||||
		<mat-icon>arrow_right</mat-icon>
 | 
			
		||||
	</button>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,141 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
 | 
			
		||||
.people-container
 | 
			
		||||
{
 | 
			
		||||
	display: flex;
 | 
			
		||||
	padding-left: 15px;
 | 
			
		||||
	padding-right: 15px;
 | 
			
		||||
	overflow-x: auto;
 | 
			
		||||
	min-width: 100%;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
	scrollbar-width: thin;
 | 
			
		||||
	scrollbar-color: #999 transparent;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar
 | 
			
		||||
	{
 | 
			
		||||
		height: 4px;
 | 
			
		||||
		background: transparent;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar-thumb
 | 
			
		||||
	{
 | 
			
		||||
		background-color: #999;
 | 
			
		||||
		border-radius: 90px;
 | 
			
		||||
 | 
			
		||||
		&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
		{
 | 
			
		||||
			background-color: rgb(134, 127, 127);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.people
 | 
			
		||||
{
 | 
			
		||||
	visibility: visible;
 | 
			
		||||
	margin: .25rem;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	color: inherit;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	flex-grow: 0;
 | 
			
		||||
	width: 33%;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		width: 22%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 20%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		width: 15%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(xl)
 | 
			
		||||
	{
 | 
			
		||||
		width: 10%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		padding-top: 147.0588%;
 | 
			
		||||
		background-size: cover;
 | 
			
		||||
		background-color: #333333;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> p, h6
 | 
			
		||||
	{
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		text-overflow: ellipsis;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
 | 
			
		||||
		&.role
 | 
			
		||||
		{
 | 
			
		||||
			font-size: 0.8em;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover, &:focus
 | 
			
		||||
	{
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
		{
 | 
			
		||||
			outline: solid var(--accentColor);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.name
 | 
			
		||||
		{
 | 
			
		||||
			text-decoration: underline;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scroll-row
 | 
			
		||||
{
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	&:host-context(.hoverEnabled) &:hover
 | 
			
		||||
	{
 | 
			
		||||
		.scrollBtn
 | 
			
		||||
		{
 | 
			
		||||
			display: block;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrollBtn
 | 
			
		||||
{
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	min-width: 0;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 30%;
 | 
			
		||||
	bottom: 40%;
 | 
			
		||||
	display: none;
 | 
			
		||||
 | 
			
		||||
	&.leftBtn
 | 
			
		||||
	{
 | 
			
		||||
		left: 0;
 | 
			
		||||
		padding-left: 10px;
 | 
			
		||||
		padding-right: 2px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.rightBtn
 | 
			
		||||
	{
 | 
			
		||||
		right: 0;
 | 
			
		||||
		padding-right: 10px;
 | 
			
		||||
		padding-left: 2px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
import { Component, Input } from "@angular/core";
 | 
			
		||||
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
 | 
			
		||||
import { People } from "../../models/resources/people";
 | 
			
		||||
import { HorizontalScroller } from "../../misc/horizontal-scroller";
 | 
			
		||||
import { Page } from "../../models/page";
 | 
			
		||||
import { HttpClient } from "@angular/common/http";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-people-list",
 | 
			
		||||
	templateUrl: "./people-list.component.html",
 | 
			
		||||
	styleUrls: ["./people-list.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class PeopleListComponent extends HorizontalScroller
 | 
			
		||||
{
 | 
			
		||||
	@Input() people: Page<People>;
 | 
			
		||||
 | 
			
		||||
	constructor(private sanitizer: DomSanitizer, public client: HttpClient)
 | 
			
		||||
	{
 | 
			
		||||
		super();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getPeopleIcon(item: People): SafeStyle
 | 
			
		||||
	{
 | 
			
		||||
		return this.sanitizer.bypassSecurityTrustStyle(`url(${item.poster})`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <div *ngFor="let show of this.shows?.items" class="show-container">
 | 
			
		||||
        <mat-card class="show">
 | 
			
		||||
            <a draggable="false" class="d-flex" (click)="this.clickCallback.emit(show)"
 | 
			
		||||
               [href]="getLink(show)" [routerLink]="getLink(show)">
 | 
			
		||||
                <div class="thumb">
 | 
			
		||||
                    <div [style.background-image]="getThumb(show)"> </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="data">
 | 
			
		||||
                    <p class="title">{{show.title}}</p>
 | 
			
		||||
                    <p class="date" *ngIf="show.endYear && show.startYear != show.endYear; else elseBlock">{{show.startYear}} - {{show.endYear}}</p>
 | 
			
		||||
                    <ng-template #elseBlock><p class="date">{{show.startYear}}</p></ng-template>
 | 
			
		||||
                    <p class="overview">{{show.overview}}</p>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        <li class="provider" *ngFor="let id of this.show.externalIDs">
 | 
			
		||||
                            <a [href]="id.link"><img [src]="id.provider.logo" [alt]="id.provider.name"/></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </a>
 | 
			
		||||
        </mat-card>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,144 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
 | 
			
		||||
button
 | 
			
		||||
{
 | 
			
		||||
	outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.arrow
 | 
			
		||||
{
 | 
			
		||||
	font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-fluid
 | 
			
		||||
{
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.show-container
 | 
			
		||||
{
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	min-width: 300px;
 | 
			
		||||
	list-style: none;
 | 
			
		||||
	padding: .5em;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		width: 50%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(xl)
 | 
			
		||||
	{
 | 
			
		||||
		width: 33%;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.show
 | 
			
		||||
{
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	
 | 
			
		||||
	> a
 | 
			
		||||
	{
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
		color: inherit;
 | 
			
		||||
		outline: none;
 | 
			
		||||
		position: relative;
 | 
			
		||||
 | 
			
		||||
		&:focus, &:hover
 | 
			
		||||
		{
 | 
			
		||||
			
 | 
			
		||||
			> .data > .title
 | 
			
		||||
			{
 | 
			
		||||
				text-decoration: underline;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .thumb
 | 
			
		||||
		{
 | 
			
		||||
			width: 33%;
 | 
			
		||||
			
 | 
			
		||||
			> div
 | 
			
		||||
			{
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				height: 0;
 | 
			
		||||
				padding-top: 147.0588%;
 | 
			
		||||
				background-size: cover;
 | 
			
		||||
				background-color: #333333;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		> .data
 | 
			
		||||
		{
 | 
			
		||||
			width: 67%;
 | 
			
		||||
			padding: .5rem;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
			right: 0;
 | 
			
		||||
 | 
			
		||||
			> p:not(.overview)
 | 
			
		||||
			{
 | 
			
		||||
				white-space: nowrap;
 | 
			
		||||
				overflow: hidden;
 | 
			
		||||
				text-overflow: ellipsis;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
				margin-bottom: 0;
 | 
			
		||||
				opacity: 1;
 | 
			
		||||
				display: inline-block;
 | 
			
		||||
				
 | 
			
		||||
				&.date
 | 
			
		||||
				{
 | 
			
		||||
					opacity: 0.8;
 | 
			
		||||
					font-size: 0.8em;
 | 
			
		||||
					padding-left: 1rem;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			> .overview
 | 
			
		||||
			{
 | 
			
		||||
				overflow-y: auto;
 | 
			
		||||
				height: calc(100% - 4rem);
 | 
			
		||||
				text-align: justify;
 | 
			
		||||
				padding-right: .5rem;
 | 
			
		||||
				margin-bottom: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:host-context(.hoverEnabled) &:hover, &:focus
 | 
			
		||||
			{
 | 
			
		||||
				cursor: pointer;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.provider
 | 
			
		||||
{
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	width: 2.5rem;
 | 
			
		||||
	height: 2.5rem;
 | 
			
		||||
	margin: .25rem;
 | 
			
		||||
 | 
			
		||||
	> a
 | 
			
		||||
	{
 | 
			
		||||
		width: 2.5rem;
 | 
			
		||||
		height: 2.5rem;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
 | 
			
		||||
		> img
 | 
			
		||||
		{
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
			right: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			margin: auto;
 | 
			
		||||
			max-width: 2.5rem;
 | 
			
		||||
			max-height: 2.5rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
 | 
			
		||||
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
 | 
			
		||||
import { Show } from "../../models/resources/show";
 | 
			
		||||
import { Page } from "../../models/page";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-shows-grid",
 | 
			
		||||
	templateUrl: "./show-grid.component.html",
 | 
			
		||||
	styleUrls: ["./show-grid.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class ShowGridComponent
 | 
			
		||||
{
 | 
			
		||||
	@Input() shows: Page<Show>;
 | 
			
		||||
	@Input() externalShows: boolean = false;
 | 
			
		||||
	@Output() clickCallback: EventEmitter<Show> = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
	constructor(private sanitizer: DomSanitizer) { }
 | 
			
		||||
 | 
			
		||||
	getThumb(show: Show): SafeStyle
 | 
			
		||||
	{
 | 
			
		||||
		return this.sanitizer.bypassSecurityTrustStyle(`url(${show.poster})`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getLink(show: Show): string
 | 
			
		||||
	{
 | 
			
		||||
		if (this.externalShows)
 | 
			
		||||
			return null;
 | 
			
		||||
		return `/show/${show.slug}`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from "@angular/router";
 | 
			
		||||
 | 
			
		||||
export class CustomRouteReuseStrategy extends RouteReuseStrategy
 | 
			
		||||
{
 | 
			
		||||
	shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean
 | 
			
		||||
	{
 | 
			
		||||
		if (curr.routeConfig?.path === "browse"
 | 
			
		||||
			|| curr.routeConfig?.path === "genre/:slug"
 | 
			
		||||
			|| curr.routeConfig?.path === "studio/:slug")
 | 
			
		||||
		{
 | 
			
		||||
			return future.routeConfig.path === "browse"
 | 
			
		||||
				|| future.routeConfig.path === "genre/:slug"
 | 
			
		||||
				|| future.routeConfig.path === "studio/:slug";
 | 
			
		||||
		}
 | 
			
		||||
		return future.routeConfig === curr.routeConfig;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shouldAttach(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shouldDetach(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store(): void  {}
 | 
			
		||||
 | 
			
		||||
	retrieve(): DetachedRouteHandle | null
 | 
			
		||||
	{
 | 
			
		||||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
 | 
			
		||||
 | 
			
		||||
/* tslint:disable:directive-selector */
 | 
			
		||||
@Directive({
 | 
			
		||||
	selector: "img[fallback]"
 | 
			
		||||
})
 | 
			
		||||
export class FallbackDirective
 | 
			
		||||
{
 | 
			
		||||
	@Input() fallback: string;
 | 
			
		||||
 | 
			
		||||
	constructor(private img: ElementRef) { }
 | 
			
		||||
 | 
			
		||||
	@HostListener("error")
 | 
			
		||||
	onError(): void
 | 
			
		||||
	{
 | 
			
		||||
		const html: HTMLImageElement = this.img.nativeElement;
 | 
			
		||||
		html.src = this.fallback;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
import { ElementRef, ViewChild } from "@angular/core";
 | 
			
		||||
import { MatButton } from "@angular/material/button";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export class HorizontalScroller
 | 
			
		||||
{
 | 
			
		||||
	@ViewChild("scrollView", { static: true }) private scrollView: ElementRef;
 | 
			
		||||
	@ViewChild("leftBtn", { static: false }) private leftBtn: MatButton;
 | 
			
		||||
	@ViewChild("rightBtn", { static: false }) private rightBtn: MatButton;
 | 
			
		||||
	@ViewChild("itemsDom", { static: false }) private itemsDom: ElementRef;
 | 
			
		||||
 | 
			
		||||
	scrollLeft(): void
 | 
			
		||||
	{
 | 
			
		||||
		const scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80);
 | 
			
		||||
		this.scrollView.nativeElement.scrollBy({ top: 0, left: -scroll, behavior: "smooth" });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scrollRight(): void
 | 
			
		||||
	{
 | 
			
		||||
		const scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80);
 | 
			
		||||
		this.scrollView.nativeElement.scrollBy({ top: 0, left: scroll, behavior: "smooth" });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	roundScroll(offset: number): number
 | 
			
		||||
	{
 | 
			
		||||
		const itemSize: number = this.itemsDom.nativeElement.scrollWidth;
 | 
			
		||||
 | 
			
		||||
		offset = Math.round(offset / itemSize) * itemSize;
 | 
			
		||||
		if (offset === 0)
 | 
			
		||||
			offset = itemSize;
 | 
			
		||||
		return offset;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onScroll(): void
 | 
			
		||||
	{
 | 
			
		||||
		const scroll: any = this.scrollView.nativeElement;
 | 
			
		||||
 | 
			
		||||
		if (scroll.scrollLeft <= 0)
 | 
			
		||||
			this.leftBtn._elementRef.nativeElement.classList.add("d-none");
 | 
			
		||||
		else
 | 
			
		||||
			this.leftBtn._elementRef.nativeElement.classList.remove("d-none");
 | 
			
		||||
		if (scroll.scrollLeft >= scroll.scrollWidth - scroll.clientWidth)
 | 
			
		||||
			this.rightBtn._elementRef.nativeElement.classList.add("d-none");
 | 
			
		||||
		else
 | 
			
		||||
			this.rightBtn._elementRef.nativeElement.classList.remove("d-none");
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,33 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
	static getLink(item: LibraryItem | Show | ShowRole | Collection): string
 | 
			
		||||
	{
 | 
			
		||||
		if ("type" in item && item.type === ItemType.Collection)
 | 
			
		||||
			return "/collection/" + item.slug;
 | 
			
		||||
		else
 | 
			
		||||
			return "/show/" + item.slug;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static getDate(item: LibraryItem | Show | ShowRole | Collection | People): string
 | 
			
		||||
	{
 | 
			
		||||
		if ("role" in item && item.role)
 | 
			
		||||
		{
 | 
			
		||||
			if ("type" in item && item.type)
 | 
			
		||||
				return `as ${item.role} (${item.type})`;
 | 
			
		||||
			return `as ${item.role}`;
 | 
			
		||||
		}
 | 
			
		||||
		if ("type" in item && item.type && typeof item.type === "string")
 | 
			
		||||
			return item.type;
 | 
			
		||||
 | 
			
		||||
		if (!("startYear" in item))
 | 
			
		||||
			return "";
 | 
			
		||||
		if (item.endYear && item.startYear !== item.endYear)
 | 
			
		||||
			return `${item.startYear} - ${item.endYear}`;
 | 
			
		||||
		return item.startYear?.toString();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,75 +0,0 @@
 | 
			
		||||
import { Directive, Output, EventEmitter, HostListener, HostBinding, ElementRef } from "@angular/core";
 | 
			
		||||
import MouseDownEvent = JQuery.MouseDownEvent;
 | 
			
		||||
import TouchStartEvent = JQuery.TouchStartEvent;
 | 
			
		||||
import ContextMenuEvent = JQuery.ContextMenuEvent;
 | 
			
		||||
import ClickEvent = JQuery.ClickEvent;
 | 
			
		||||
 | 
			
		||||
function cancelClick(event: ClickEvent): void
 | 
			
		||||
{
 | 
			
		||||
	event.preventDefault();
 | 
			
		||||
	event.stopPropagation();
 | 
			
		||||
	this.removeEventListener("click", cancelClick, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Directive({
 | 
			
		||||
	selector: `[appLongPress]`
 | 
			
		||||
})
 | 
			
		||||
export class LongPressDirective
 | 
			
		||||
{
 | 
			
		||||
	@Output() longPressed = new EventEmitter();
 | 
			
		||||
	private _timer: NodeJS.Timeout = null;
 | 
			
		||||
 | 
			
		||||
	constructor(private ref: ElementRef) {}
 | 
			
		||||
 | 
			
		||||
	@HostBinding("style.-webkit-touch-callout")
 | 
			
		||||
	defaultLongTouchEvent: string = "none";
 | 
			
		||||
 | 
			
		||||
	@HostBinding("class.longpress")
 | 
			
		||||
	get longPress(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return this._timer !== null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("touchstart", ["$event"])
 | 
			
		||||
	@HostListener("mousedown", ["$event"])
 | 
			
		||||
	start(event: MouseDownEvent | TouchStartEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		const startBox: DOMRect = event.target.getBoundingClientRect();
 | 
			
		||||
		this._timer = setTimeout(() =>
 | 
			
		||||
		{
 | 
			
		||||
			const endBox: DOMRect = event.target.getBoundingClientRect();
 | 
			
		||||
			if (startBox.top !== endBox.top || startBox.left !== endBox.left)
 | 
			
		||||
				return;
 | 
			
		||||
			this.longPressed.emit();
 | 
			
		||||
			this._timer = null;
 | 
			
		||||
			this.ref.nativeElement.addEventListener("click", cancelClick, true);
 | 
			
		||||
		}, 500);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("touchend", ["$event"])
 | 
			
		||||
	@HostListener("window:mouseup", ["$event"])
 | 
			
		||||
	end(): void
 | 
			
		||||
	{
 | 
			
		||||
		setTimeout(() =>
 | 
			
		||||
		{
 | 
			
		||||
			this.ref.nativeElement.removeEventListener("click", cancelClick, true);
 | 
			
		||||
		}, 50);
 | 
			
		||||
		this.cancel();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("wheel", ["$event"])
 | 
			
		||||
	@HostListener("scroll", ["$event"])
 | 
			
		||||
	@HostListener("document.scroll", ["$event"])
 | 
			
		||||
	@HostListener("window.scroll", ["$event"])
 | 
			
		||||
	cancel(): void
 | 
			
		||||
	{
 | 
			
		||||
		clearTimeout(this._timer);
 | 
			
		||||
		this._timer = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("contextmenu", ["$event"])
 | 
			
		||||
	context(event: ContextMenuEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms";
 | 
			
		||||
import { Directive } from "@angular/core";
 | 
			
		||||
 | 
			
		||||
@Directive({
 | 
			
		||||
	selector: "[appPasswordValidator]",
 | 
			
		||||
	providers: [{provide: NG_VALIDATORS, useExisting: PasswordValidator, multi: true}]
 | 
			
		||||
})
 | 
			
		||||
export class PasswordValidator implements Validator
 | 
			
		||||
{
 | 
			
		||||
	validate(control: AbstractControl): {[key: string]: any} | null
 | 
			
		||||
	{
 | 
			
		||||
		if (!control.value)
 | 
			
		||||
			return null;
 | 
			
		||||
		if (!/[a-z]/.test(control.value))
 | 
			
		||||
			return {passwordError: {error: "The password must contains a lowercase letter."}};
 | 
			
		||||
		if (!/[A-Z]/.test(control.value))
 | 
			
		||||
			return {passwordError: {error: "The password must contains an uppercase letter."}};
 | 
			
		||||
		if (!/[0-9]/.test(control.value))
 | 
			
		||||
			return {passwordError: {error: "The password must contains a digit."}};
 | 
			
		||||
		if (!/\W/.test(control.value))
 | 
			
		||||
			return {passwordError: {error: "The password must contains a non-alphanumeric character."}};
 | 
			
		||||
		if (control.value.toString().length < 6)
 | 
			
		||||
			return {passwordError: {error: "Password must be at least 6 character long."}};
 | 
			
		||||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +0,0 @@
 | 
			
		||||
export interface Account
 | 
			
		||||
{
 | 
			
		||||
	username: string;
 | 
			
		||||
	email: string;
 | 
			
		||||
	picture: string;
 | 
			
		||||
	permissions: string[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
import { Provider } from "./provider";
 | 
			
		||||
 | 
			
		||||
export interface ExternalID
 | 
			
		||||
{
 | 
			
		||||
	provider: Provider;
 | 
			
		||||
	dataID: string;
 | 
			
		||||
	link: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
import { HttpClient } from "@angular/common/http";
 | 
			
		||||
 | 
			
		||||
export class Page<T>
 | 
			
		||||
{
 | 
			
		||||
	this: string;
 | 
			
		||||
	next: string;
 | 
			
		||||
	first: string;
 | 
			
		||||
	count: number;
 | 
			
		||||
	items: T[];
 | 
			
		||||
	private _isLoading: boolean = false;
 | 
			
		||||
 | 
			
		||||
	constructor(init?: Partial<Page<T>>)
 | 
			
		||||
	{
 | 
			
		||||
		Object.assign(this, init);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	loadNext(client: HttpClient): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.next == null || this._isLoading)
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		this._isLoading = true;
 | 
			
		||||
		client.get<Page<T>>(this.next).subscribe(x =>
 | 
			
		||||
		{
 | 
			
		||||
			this.items.push(...x.items);
 | 
			
		||||
			this.count += x.count;
 | 
			
		||||
			this.next = x.next;
 | 
			
		||||
			this.this = x.this;
 | 
			
		||||
			this._isLoading = false;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	changeType(type: string): string
 | 
			
		||||
	{
 | 
			
		||||
		return this.first.replace(/\/\w*($|\?)/, `/${type}$1`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
export interface Provider
 | 
			
		||||
{
 | 
			
		||||
	name: string;
 | 
			
		||||
	logo: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=collection.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"collection.js","sourceRoot":"","sources":["collection.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
import { Show } from "./show";
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Collection extends IResource
 | 
			
		||||
{
 | 
			
		||||
	name: string;
 | 
			
		||||
	poster: string;
 | 
			
		||||
	overview: string;
 | 
			
		||||
	startYear: number;
 | 
			
		||||
	endYear: number;
 | 
			
		||||
	shows: Show[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=episode.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"episode.js","sourceRoot":"","sources":["episode.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
import { ExternalID } from "../external-id";
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Episode extends IResource
 | 
			
		||||
{
 | 
			
		||||
	seasonNumber: number;
 | 
			
		||||
	episodeNumber: number;
 | 
			
		||||
	title: string;
 | 
			
		||||
	thumb: string;
 | 
			
		||||
	overview: string;
 | 
			
		||||
	releaseDate: string;
 | 
			
		||||
	runtime: number;
 | 
			
		||||
	showTitle: string;
 | 
			
		||||
	externalIDs: ExternalID[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=genre.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"genre.js","sourceRoot":"","sources":["genre.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Genre extends IResource
 | 
			
		||||
{
 | 
			
		||||
	name: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export enum ItemType
 | 
			
		||||
{
 | 
			
		||||
	Show,
 | 
			
		||||
	Movie,
 | 
			
		||||
	Collection
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LibraryItem extends IResource
 | 
			
		||||
{
 | 
			
		||||
	title: string;
 | 
			
		||||
	overview: string;
 | 
			
		||||
	status: string;
 | 
			
		||||
	trailerUrl: string;
 | 
			
		||||
	startYear: number;
 | 
			
		||||
	endYear: number;
 | 
			
		||||
	poster: string;
 | 
			
		||||
	type: ItemType;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Library extends IResource
 | 
			
		||||
{
 | 
			
		||||
	id: number;
 | 
			
		||||
	slug: string;
 | 
			
		||||
	name: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=people.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"people.js","sourceRoot":"","sources":["people.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
import { ExternalID } from "../external-id";
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
import { Show } from "./show";
 | 
			
		||||
 | 
			
		||||
export interface People extends IResource
 | 
			
		||||
{
 | 
			
		||||
	name: string;
 | 
			
		||||
	role: string;
 | 
			
		||||
	type: string;
 | 
			
		||||
	poster: string;
 | 
			
		||||
 | 
			
		||||
	shows: Show;
 | 
			
		||||
	externalIDs: ExternalID[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
export interface IResource
 | 
			
		||||
{
 | 
			
		||||
	id: number;
 | 
			
		||||
	slug: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=season.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"season.js","sourceRoot":"","sources":["season.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
import { Episode } from "./episode";
 | 
			
		||||
import { ExternalID } from "../external-id";
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Season extends IResource
 | 
			
		||||
{
 | 
			
		||||
	seasonNumber: number;
 | 
			
		||||
	title: string;
 | 
			
		||||
	overview: string;
 | 
			
		||||
	episodes: Episode[];
 | 
			
		||||
	externalIDs: ExternalID[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=show.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"show.js","sourceRoot":"","sources":["show.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,45 +0,0 @@
 | 
			
		||||
import { Season } from "./season";
 | 
			
		||||
import { Genre } from "./genre";
 | 
			
		||||
import { People } from "./people";
 | 
			
		||||
import { Studio } from "./studio";
 | 
			
		||||
import { ExternalID } from "../external-id";
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Show extends IResource
 | 
			
		||||
{
 | 
			
		||||
	title: string;
 | 
			
		||||
	aliases: string[];
 | 
			
		||||
	overview: string;
 | 
			
		||||
	genres: Genre[];
 | 
			
		||||
	status: string;
 | 
			
		||||
	studio: Studio;
 | 
			
		||||
	people: People[];
 | 
			
		||||
	seasons: Season[];
 | 
			
		||||
	trailerUrl: string;
 | 
			
		||||
	isMovie: boolean;
 | 
			
		||||
	startYear: number;
 | 
			
		||||
	endYear: number;
 | 
			
		||||
	poster: string;
 | 
			
		||||
	logo: string;
 | 
			
		||||
	backdrop: string;
 | 
			
		||||
 | 
			
		||||
	externalIDs: ExternalID[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ShowRole extends IResource
 | 
			
		||||
{
 | 
			
		||||
	role: string;
 | 
			
		||||
	type: string;
 | 
			
		||||
 | 
			
		||||
	title: string;
 | 
			
		||||
	aliases: string[];
 | 
			
		||||
	overview: string;
 | 
			
		||||
	status: string;
 | 
			
		||||
	trailerUrl: string;
 | 
			
		||||
	isMovie: boolean;
 | 
			
		||||
	startYear: number;
 | 
			
		||||
	endYear: number;
 | 
			
		||||
	poster: string;
 | 
			
		||||
	logo: string;
 | 
			
		||||
	backdrop: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=studio.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"studio.js","sourceRoot":"","sources":["studio.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
import { IResource } from "./resource";
 | 
			
		||||
 | 
			
		||||
export interface Studio extends IResource
 | 
			
		||||
{
 | 
			
		||||
	name: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=search-result.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"search-result.js","sourceRoot":"","sources":["search-result.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
import { Show } from "./resources/show";
 | 
			
		||||
import { Episode } from "./resources/episode";
 | 
			
		||||
import { People } from "./resources/people";
 | 
			
		||||
import { Studio } from "./resources/studio";
 | 
			
		||||
import { Genre } from "./resources/genre";
 | 
			
		||||
import { Collection } from "./resources/collection";
 | 
			
		||||
 | 
			
		||||
export interface SearchResult
 | 
			
		||||
{
 | 
			
		||||
	query: string;
 | 
			
		||||
	collections: Collection[];
 | 
			
		||||
	shows: Show[];
 | 
			
		||||
	episodes: Episode[];
 | 
			
		||||
	people: People[];
 | 
			
		||||
	genres: Genre[];
 | 
			
		||||
	studios: Studio[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
Object.defineProperty(exports, "__esModule", { value: true });
 | 
			
		||||
//# sourceMappingURL=watch-item.js.map
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
{"version":3,"file":"watch-item.js","sourceRoot":"","sources":["watch-item.ts"],"names":[],"mappings":""}
 | 
			
		||||
@ -1,33 +0,0 @@
 | 
			
		||||
import { Episode } from "./resources/episode";
 | 
			
		||||
 | 
			
		||||
export interface WatchItem
 | 
			
		||||
{
 | 
			
		||||
	showTitle: string;
 | 
			
		||||
	showSlug: string;
 | 
			
		||||
	seasonNumber: number;
 | 
			
		||||
	episodeNumber: number;
 | 
			
		||||
	title: string;
 | 
			
		||||
	slug: string;
 | 
			
		||||
	duration: number;
 | 
			
		||||
	releaseDate: string;
 | 
			
		||||
	isMovie: boolean;
 | 
			
		||||
 | 
			
		||||
	previousEpisode: Episode;
 | 
			
		||||
	nextEpisode: Episode;
 | 
			
		||||
 | 
			
		||||
	container: string;
 | 
			
		||||
	video: Track;
 | 
			
		||||
	audios: Track[];
 | 
			
		||||
	subtitles: Track[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Track
 | 
			
		||||
{
 | 
			
		||||
	displayName: string;
 | 
			
		||||
	title: string;
 | 
			
		||||
	language: string;
 | 
			
		||||
	isDefault: boolean;
 | 
			
		||||
	isForced: boolean;
 | 
			
		||||
	codec: string;
 | 
			
		||||
	slug: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
	<div class="row justify-content-center">
 | 
			
		||||
		<div class="col-md-4 col-lg-3 col-xl-2 collection-info">
 | 
			
		||||
			<div [style.background-image]="getThumb()"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="col-md-8 col-lg-9 col-xl-10">
 | 
			
		||||
			<h3 class="text-center text-md-left p-2 p-md-3">{{collection.name}}</h3>
 | 
			
		||||
			<h5 class="date">{{getDate(collection)}}</h5>
 | 
			
		||||
			<hr />
 | 
			
		||||
			<app-items-grid [page]="shows"></app-items-grid>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
.collection-info
 | 
			
		||||
{
 | 
			
		||||
	width: 60%;
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		padding-top: 147.0588%;
 | 
			
		||||
		background-size: cover;
 | 
			
		||||
		background-color: #333333;
 | 
			
		||||
		margin: 10px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
hr
 | 
			
		||||
{
 | 
			
		||||
	margin: 10px 0 10px 0;
 | 
			
		||||
	border-top: 1px solid rgba(255, 255, 255, .60);
 | 
			
		||||
	border-left: 0;
 | 
			
		||||
	width: inherit;
 | 
			
		||||
	height: 2px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,39 +0,0 @@
 | 
			
		||||
import { Component } from "@angular/core";
 | 
			
		||||
import { Collection } from "../../models/resources/collection";
 | 
			
		||||
import { ActivatedRoute } from "@angular/router";
 | 
			
		||||
import { DomSanitizer, SafeStyle } 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 { ItemsUtils } from "../../misc/items-utils";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-collection",
 | 
			
		||||
	templateUrl: "./collection.component.html",
 | 
			
		||||
	styleUrls: ["./collection.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class CollectionComponent
 | 
			
		||||
{
 | 
			
		||||
	collection: Collection | People;
 | 
			
		||||
	shows: Page<Show>;
 | 
			
		||||
 | 
			
		||||
	constructor(private route: ActivatedRoute, private sanitizer: DomSanitizer)
 | 
			
		||||
	{
 | 
			
		||||
		this.route.data.subscribe((data) =>
 | 
			
		||||
		{
 | 
			
		||||
			this.collection = data.collection;
 | 
			
		||||
			this.shows = data.shows;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getThumb(): SafeStyle
 | 
			
		||||
	{
 | 
			
		||||
		return this.sanitizer.bypassSecurityTrustStyle(`url(${this.collection.poster})`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getDate(item: LibraryItem | Show | ShowRole | Collection | People): string
 | 
			
		||||
	{
 | 
			
		||||
		return ItemsUtils.getDate(item);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,116 +0,0 @@
 | 
			
		||||
<!--suppress TypeScriptUnresolvedVariable ($event.target.value does exist) -->
 | 
			
		||||
<h2 mat-dialog-title>Editing metadata of {{this.show.title}}</h2>
 | 
			
		||||
<div matDialogContent class="pb-4">
 | 
			
		||||
    <mat-accordion>
 | 
			
		||||
        <mat-expansion-panel expanded="true">
 | 
			
		||||
            <mat-expansion-panel-header>
 | 
			
		||||
                <mat-panel-title>Edit metadata</mat-panel-title>
 | 
			
		||||
                <mat-panel-description>Manually edit each property</mat-panel-description>
 | 
			
		||||
            </mat-expansion-panel-header>
 | 
			
		||||
            <form #showForm="ngForm" class="pt-3">
 | 
			
		||||
                <mat-form-field class="w-100">
 | 
			
		||||
                    <mat-label>Title</mat-label>
 | 
			
		||||
                    <input matInput [(ngModel)]="this.show.title" name="title">
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
 | 
			
		||||
                <mat-form-field class="w-100">
 | 
			
		||||
                    <mat-label>Aliases</mat-label>
 | 
			
		||||
                    <mat-chip-list #aliasList>
 | 
			
		||||
                        <mat-chip *ngFor="let alias of this.show.aliases" (removed)="removeAlias(alias)" removable="true">
 | 
			
		||||
                            {{alias}}
 | 
			
		||||
                            <mat-icon matChipRemove>cancel</mat-icon>
 | 
			
		||||
                        </mat-chip>
 | 
			
		||||
                        <input placeholder="New alias..." [matChipInputFor]="aliasList" (matChipInputTokenEnd)="addAlias($event)" />
 | 
			
		||||
                    </mat-chip-list>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
 | 
			
		||||
                <mat-form-field class="w-100">
 | 
			
		||||
                    <mat-label>Overview</mat-label>
 | 
			
		||||
                    <textarea matInput [(ngModel)]="this.show.overview" name="overview"></textarea>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
        
 | 
			
		||||
                <mat-form-field class="w-25 pr-3">
 | 
			
		||||
                    <mat-label>Start Year</mat-label>
 | 
			
		||||
                    <input matInput [(ngModel)]="this.show.startYear" name="startYear" type="number" [max]="this.show.endYear"/>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
                <mat-form-field class="w-25 pr-3">
 | 
			
		||||
                    <mat-label>End Year</mat-label>
 | 
			
		||||
                    <input matInput [(ngModel)]="this.show.endYear" name="endYear" type="number" [min]="this.show.startYear"/>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
                <mat-form-field class="w-50">
 | 
			
		||||
                    <mat-label>Status</mat-label>
 | 
			
		||||
                    <mat-select>
 | 
			
		||||
                        <mat-option value="Finished">Finished</mat-option>
 | 
			
		||||
                        <mat-option value="Airing">Airing</mat-option>
 | 
			
		||||
                        <mat-option value="Planned">Planned</mat-option>
 | 
			
		||||
                    </mat-select>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
                
 | 
			
		||||
                <mat-form-field class="w-100">
 | 
			
		||||
                    <mat-label>Genres</mat-label>
 | 
			
		||||
                    <mat-chip-list #genreList>
 | 
			
		||||
                        <mat-chip *ngFor="let genre of this.show.genres" (removed)="removeGenre(genre)" removable="true">
 | 
			
		||||
                            {{genre.name}}
 | 
			
		||||
                            <mat-icon matChipRemove>cancel</mat-icon>
 | 
			
		||||
                        </mat-chip>
 | 
			
		||||
                        <input #genreInput placeholder="New genre..."
 | 
			
		||||
                               [formControl]="genreForm"
 | 
			
		||||
                               [matChipInputFor]="genreList"
 | 
			
		||||
                               (matChipInputTokenEnd)="addGenre($event); $event.input.value = null;"
 | 
			
		||||
                               [matAutocomplete]="genreAuto" />
 | 
			
		||||
                        <mat-autocomplete #genreAuto="matAutocomplete"
 | 
			
		||||
                                          (optionSelected)="autocompleteGenre($event); genreInput.value = null;">
 | 
			
		||||
	                        <mat-option *ngFor="let genre of this.filteredGenres | async" [value]="genre">
 | 
			
		||||
                                {{genre.name}}
 | 
			
		||||
                            </mat-option>
 | 
			
		||||
                        </mat-autocomplete>
 | 
			
		||||
                    </mat-chip-list>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
        
 | 
			
		||||
                <mat-form-field class="w-100">
 | 
			
		||||
                    <mat-label>Trailer</mat-label>
 | 
			
		||||
                    <input matInput [(ngModel)]="this.show.trailerUrl" name="trailer">
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
        
 | 
			
		||||
                <mat-form-field class="w-100">
 | 
			
		||||
                    <mat-label>Studio</mat-label>
 | 
			
		||||
                    <input matInput [value]="this.show.studio?.name"
 | 
			
		||||
                           [formControl]="studioForm"
 | 
			
		||||
                           (input)="this.show.studio = {id: 0, slug: null, name: $event.target.value};"
 | 
			
		||||
                           [matAutocomplete]="studioAuto" name="studio">
 | 
			
		||||
                    <mat-autocomplete #studioAuto="matAutocomplete" (optionSelected)="this.show.studio = $event.option.value">
 | 
			
		||||
                        <mat-option *ngFor="let studio of this.filteredStudios | async" [value]="studio">
 | 
			
		||||
                            {{studio.name}}
 | 
			
		||||
                        </mat-option>
 | 
			
		||||
                    </mat-autocomplete>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
            </form>
 | 
			
		||||
        </mat-expansion-panel>
 | 
			
		||||
        <mat-expansion-panel>
 | 
			
		||||
            <mat-expansion-panel-header>
 | 
			
		||||
                <mat-panel-title>Identify show</mat-panel-title>
 | 
			
		||||
                <mat-panel-description>Search on metadata providers</mat-panel-description>
 | 
			
		||||
            </mat-expansion-panel-header>
 | 
			
		||||
            <mat-form-field class="w-100 mx-2">
 | 
			
		||||
                <mat-label>Search for</mat-label>
 | 
			
		||||
                <input matInput value="{{show.title}}" (input)="this.reIdentify($event.target.value)">
 | 
			
		||||
            </mat-form-field>
 | 
			
		||||
            <mat-form-field *ngFor="let provider of this.providers" class="provider px-1">
 | 
			
		||||
                <mat-label>{{provider.name}} ID</mat-label>
 | 
			
		||||
                <input matInput [value]="this.getMetadataID(provider)?.dataID" (input)="this.setMetadataID(provider, $event.target.value)" >
 | 
			
		||||
            </mat-form-field>
 | 
			
		||||
            <app-shows-grid #identifyGrid [externalShows]="true" (clickCallback)="this.identifyID($event)"></app-shows-grid>
 | 
			
		||||
        </mat-expansion-panel>
 | 
			
		||||
    </mat-accordion>
 | 
			
		||||
</div>
 | 
			
		||||
<div mat-dialog-actions align="end">
 | 
			
		||||
	<button mat-button mat-dialog-close>Cancel</button>
 | 
			
		||||
	<button mat-button (click)="apply()">
 | 
			
		||||
        <mat-icon *ngIf="this.metadataChanged"
 | 
			
		||||
                  style="color: red;"
 | 
			
		||||
                  class="mr-2"
 | 
			
		||||
                  matTooltip="You changed an external id, the whole show's metadata will be refreshed. Individual changes made won't last."
 | 
			
		||||
                  matTooltipPosition="above">warning</mat-icon>
 | 
			
		||||
        Apply
 | 
			
		||||
    </button>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
 | 
			
		||||
.provider
 | 
			
		||||
{
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 33%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		width: 25%;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,188 +0,0 @@
 | 
			
		||||
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
 | 
			
		||||
import { FormControl } from "@angular/forms";
 | 
			
		||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
 | 
			
		||||
import { HttpClient } from "@angular/common/http";
 | 
			
		||||
import { Page } from "../../models/page";
 | 
			
		||||
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 { catchError, filter, map, mergeAll, tap } from "rxjs/operators";
 | 
			
		||||
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";
 | 
			
		||||
import { GenreService, ShowService, StudioService } from "../../services/api.service";
 | 
			
		||||
import { ExternalID } from "../../models/external-id";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-metadata-edit",
 | 
			
		||||
	templateUrl: "./metadata-edit.component.html",
 | 
			
		||||
	styleUrls: ["./metadata-edit.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class MetadataEditComponent implements OnInit
 | 
			
		||||
{
 | 
			
		||||
	studioForm: FormControl = new FormControl();
 | 
			
		||||
	filteredStudios: Observable<Studio[]>;
 | 
			
		||||
 | 
			
		||||
	genreForm: FormControl = new FormControl();
 | 
			
		||||
	filteredGenres: Observable<Genre[]>;
 | 
			
		||||
 | 
			
		||||
	@ViewChild("identifyGrid") private identifyGrid: ShowGridComponent;
 | 
			
		||||
	private _identifying: Observable<Show[]>;
 | 
			
		||||
	private _identifiedShows: [string, Show[]];
 | 
			
		||||
	public providers: Provider[] = [];
 | 
			
		||||
 | 
			
		||||
	public metadataChanged: boolean = false;
 | 
			
		||||
 | 
			
		||||
	constructor(public dialogRef: MatDialogRef<MetadataEditComponent>,
 | 
			
		||||
	            @Inject(MAT_DIALOG_DATA) public show: Show,
 | 
			
		||||
	            private http: HttpClient,
 | 
			
		||||
	            private showsApi: ShowService,
 | 
			
		||||
	            private studioApi: StudioService,
 | 
			
		||||
	            private genreApi: GenreService,
 | 
			
		||||
	            private snackBar: MatSnackBar)
 | 
			
		||||
	{
 | 
			
		||||
		this.http.get<Page<Provider>>("/api/providers").subscribe(result =>
 | 
			
		||||
		{
 | 
			
		||||
			this.providers = result.items;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.reIdentify(this.show.title);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ngOnInit(): void
 | 
			
		||||
	{
 | 
			
		||||
		this.filteredGenres = this.genreForm.valueChanges
 | 
			
		||||
			.pipe(
 | 
			
		||||
				filter(x => x),
 | 
			
		||||
				map(x => typeof x === "string" ? x : x.name),
 | 
			
		||||
				map(x => this.genreApi.search(x)),
 | 
			
		||||
				mergeAll(),
 | 
			
		||||
				catchError(x =>
 | 
			
		||||
				{
 | 
			
		||||
					console.log(x);
 | 
			
		||||
					return [];
 | 
			
		||||
				})
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
		this.filteredStudios = this.studioForm.valueChanges
 | 
			
		||||
			.pipe(
 | 
			
		||||
				filter(x => x),
 | 
			
		||||
				map(x => typeof x === "string" ? x : x.name),
 | 
			
		||||
				map(x => this.studioApi.search(x)),
 | 
			
		||||
				mergeAll(),
 | 
			
		||||
				catchError(x =>
 | 
			
		||||
				{
 | 
			
		||||
					console.log(x);
 | 
			
		||||
					return [];
 | 
			
		||||
				})
 | 
			
		||||
			);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apply(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.metadataChanged)
 | 
			
		||||
		{
 | 
			
		||||
			this.http.post("/api/show/re-identify/" + this.show.slug, this.show.externalIDs).subscribe(
 | 
			
		||||
				() => {},
 | 
			
		||||
				() =>
 | 
			
		||||
				{
 | 
			
		||||
					this.snackBar.open("An unknown error occurred.", null, {
 | 
			
		||||
						horizontalPosition: "left",
 | 
			
		||||
						panelClass: ["snackError"],
 | 
			
		||||
						duration: 2500
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			);
 | 
			
		||||
			this.dialogRef.close(this.show);
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			this.showsApi.edit(this.show).subscribe(() =>
 | 
			
		||||
			{
 | 
			
		||||
				this.dialogRef.close(this.show);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addAlias(event: MatChipInputEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		const input: HTMLInputElement = event.input;
 | 
			
		||||
		const value: string = event.value;
 | 
			
		||||
 | 
			
		||||
		this.show.aliases.push(value);
 | 
			
		||||
		if (input)
 | 
			
		||||
			input.value = "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	removeAlias(alias: string): void
 | 
			
		||||
	{
 | 
			
		||||
		const i: number = this.show.aliases.indexOf(alias);
 | 
			
		||||
		this.show.aliases.splice(i, 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addGenre(event: MatChipInputEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		const input: HTMLInputElement = event.input;
 | 
			
		||||
		const value: string = event.value;
 | 
			
		||||
		const genre: Genre = {id: 0, slug: null, name: value};
 | 
			
		||||
 | 
			
		||||
		this.show.genres.push(genre);
 | 
			
		||||
		if (input)
 | 
			
		||||
			input.value = "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	removeGenre(genre: Genre): void
 | 
			
		||||
	{
 | 
			
		||||
		const i: number = this.show.genres.indexOf(genre);
 | 
			
		||||
		this.show.genres.splice(i, 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	autocompleteGenre(event: MatAutocompleteSelectedEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		this.show.genres.push(event.option.value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	identityShow(name: string): Observable<Show[]>
 | 
			
		||||
	{
 | 
			
		||||
		if (this._identifiedShows && this._identifiedShows[0] === name)
 | 
			
		||||
			return of(this._identifiedShows[1]);
 | 
			
		||||
		this._identifying = this.http.get<Show[]>("/api/show/identify/" + name + "?isMovie=" + this.show.isMovie).pipe(
 | 
			
		||||
			tap(result => this._identifiedShows = [name, result])
 | 
			
		||||
		);
 | 
			
		||||
		return this._identifying;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	reIdentify(search: string): void
 | 
			
		||||
	{
 | 
			
		||||
		// TODO implement this
 | 
			
		||||
		// this.identityShow(search).subscribe(x => this.identifyGrid.shows = x);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getMetadataID(provider: Provider): ExternalID
 | 
			
		||||
	{
 | 
			
		||||
		return this.show.externalIDs.find(x => x.provider.name === provider.name);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setMetadataID(provider: Provider, id: string, link: string = null): void
 | 
			
		||||
	{
 | 
			
		||||
		const i: number = this.show.externalIDs.findIndex(x => x.provider.name === provider.name);
 | 
			
		||||
 | 
			
		||||
		this.metadataChanged = true;
 | 
			
		||||
		if (i !== -1)
 | 
			
		||||
		{
 | 
			
		||||
			this.show.externalIDs[i].dataID = id;
 | 
			
		||||
			this.show.externalIDs[i].link = link;
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
			this.show.externalIDs.push({provider, dataID: id, link});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	identifyID(show: Show): void
 | 
			
		||||
	{
 | 
			
		||||
		for (const id of show.externalIDs)
 | 
			
		||||
			this.setMetadataID(id.provider, id.dataID, id.link);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<br/>
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
	<h1>404 Error</h1>
 | 
			
		||||
	<p>The page you requested was not found.</p>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import { Component } from "@angular/core";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-not-found",
 | 
			
		||||
	templateUrl: "./not-found.component.html",
 | 
			
		||||
	styleUrls: ["./not-found.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class NotFoundComponent
 | 
			
		||||
{
 | 
			
		||||
	constructor() { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,155 +0,0 @@
 | 
			
		||||
import { BotInfo, BrowserInfo, detect, NodeInfo, ReactNativeInfo, SearchBotDeviceInfo } from "detect-browser";
 | 
			
		||||
import { Track, WatchItem } from "../../models/watch-item";
 | 
			
		||||
 | 
			
		||||
export enum method
 | 
			
		||||
{
 | 
			
		||||
	direct = "Direct",
 | 
			
		||||
	transmux = "Transmux",
 | 
			
		||||
	transcode = "Transcode"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SupportList
 | 
			
		||||
{
 | 
			
		||||
	container: boolean;
 | 
			
		||||
	videoCodec: boolean;
 | 
			
		||||
	audioCodec: boolean[];
 | 
			
		||||
 | 
			
		||||
	getPlaybackMethod(): method
 | 
			
		||||
	{
 | 
			
		||||
		if (this.container)
 | 
			
		||||
		{
 | 
			
		||||
			if (this.videoCodec && this.audioCodec)
 | 
			
		||||
				return method.direct;
 | 
			
		||||
			return method.transcode;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.videoCodec && this.audioCodec)
 | 
			
		||||
			return method.transmux;
 | 
			
		||||
		return method.transcode;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getWhatIsSupported(player: HTMLVideoElement, item: WatchItem): SupportList
 | 
			
		||||
{
 | 
			
		||||
	const supportList: SupportList = new SupportList();
 | 
			
		||||
	const browser: BrowserInfo | SearchBotDeviceInfo | BotInfo | NodeInfo | ReactNativeInfo = detect();
 | 
			
		||||
 | 
			
		||||
	if (!browser)
 | 
			
		||||
	{
 | 
			
		||||
		supportList.container = false;
 | 
			
		||||
		supportList.videoCodec = false;
 | 
			
		||||
		supportList.audioCodec = item.audios.map(() => false);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		supportList.container = containerIsSupported(player, item.container, browser.name) && item.audios.length <= 1;
 | 
			
		||||
		supportList.videoCodec = videoCodecIsSupported(player, item.video.codec, browser.name);
 | 
			
		||||
		supportList.audioCodec = item.audios.map((x: Track) => audioCodecIsSupported(player, x.codec, browser.name));
 | 
			
		||||
	}
 | 
			
		||||
	return (supportList);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function containerIsSupported(player: HTMLVideoElement, container: string, browser: string): boolean
 | 
			
		||||
{
 | 
			
		||||
	switch (container)
 | 
			
		||||
	{
 | 
			
		||||
		case "asf":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge";
 | 
			
		||||
		case "avi":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge";
 | 
			
		||||
		case "mpg":
 | 
			
		||||
		case "mpeg":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge";
 | 
			
		||||
		case "flv":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay";
 | 
			
		||||
		case "3gp":
 | 
			
		||||
		case "mts":
 | 
			
		||||
		case "trp":
 | 
			
		||||
		case "vob":
 | 
			
		||||
		case "vro":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay";
 | 
			
		||||
		case "mov":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge" || browser === "chrome";
 | 
			
		||||
		case "m2ts":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge";
 | 
			
		||||
		case "wmv":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge";
 | 
			
		||||
		case "ts":
 | 
			
		||||
			return browser === "tizen" || browser === "orsay" || browser === "edge";
 | 
			
		||||
		case "mp4":
 | 
			
		||||
		case "m4v":
 | 
			
		||||
			return true;
 | 
			
		||||
		case "mkv":
 | 
			
		||||
			if (browser === "tizen" || browser === "orsay" || browser === "chrome" || browser === "edge")
 | 
			
		||||
				return true;
 | 
			
		||||
			return !!(player.canPlayType("video/x-matroska") || player.canPlayType("video/mkv"));
 | 
			
		||||
 | 
			
		||||
		default:
 | 
			
		||||
			return false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SHOULD CHECK FOR DEPTH (8bits ok but 10bits unsupported for almost every browsers)
 | 
			
		||||
function videoCodecIsSupported(player: HTMLVideoElement, codec: string, browser: string): boolean
 | 
			
		||||
{
 | 
			
		||||
	switch (codec)
 | 
			
		||||
	{
 | 
			
		||||
		case "h264":
 | 
			
		||||
			return !!player.canPlayType("video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"");
 | 
			
		||||
		case "h265":
 | 
			
		||||
		case "hevc":
 | 
			
		||||
			if (browser === "tizen" || browser === "orsay" || browser === "xboxOne" || browser === "ios")
 | 
			
		||||
				return true;
 | 
			
		||||
			// SHOULD SUPPORT CHROMECAST ULTRA
 | 
			
		||||
			// 	if (browser.chromecast)
 | 
			
		||||
			// 	{
 | 
			
		||||
 | 
			
		||||
			// 		var isChromecastUltra = userAgent.indexOf('aarch64') !=== -1;
 | 
			
		||||
			// 		if (isChromecastUltra)
 | 
			
		||||
			// 		{
 | 
			
		||||
			// 			return true;
 | 
			
		||||
			// 		}
 | 
			
		||||
			// 	}
 | 
			
		||||
			return !!player.canPlayType("video/hevc; codecs=\"hevc, aac\"");
 | 
			
		||||
		case "mpeg2video":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen" || browser === "edge";
 | 
			
		||||
		case "vc1":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen" || browser === "edge";
 | 
			
		||||
		case "msmpeg4v2":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen";
 | 
			
		||||
		case "vp8":
 | 
			
		||||
			return !!player.canPlayType("video/webm; codecs=\"vp8");
 | 
			
		||||
		case "vp9":
 | 
			
		||||
			return !!player.canPlayType("video/webm; codecs=\"vp9\"");
 | 
			
		||||
		case "vorbis":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen" || !!player.canPlayType("video/webm; codecs=\"vp8");
 | 
			
		||||
		default:
 | 
			
		||||
			return false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SHOULD CHECK FOR NUMBER OF AUDIO CHANNEL (2 ok but 5 not in some browsers)
 | 
			
		||||
function audioCodecIsSupported(player: HTMLVideoElement, codec: string, browser: string): boolean
 | 
			
		||||
{
 | 
			
		||||
	switch (codec)
 | 
			
		||||
	{
 | 
			
		||||
		case "mp3":
 | 
			
		||||
			return !!player.canPlayType("video/mp4; codecs=\"avc1.640029, mp4a.69\"") ||
 | 
			
		||||
				!!player.canPlayType("video/mp4; codecs=\"avc1.640029, mp4a.6B\"");
 | 
			
		||||
		case "aac":
 | 
			
		||||
			return !!player.canPlayType("video/mp4; codecs=\"avc1.640029, mp4a.40.2\"");
 | 
			
		||||
		case "mp2":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen" || browser === "edge";
 | 
			
		||||
		case "pcm_s16le":
 | 
			
		||||
		case "pcm_s24le":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen" || browser === "edge";
 | 
			
		||||
		case "aac_latm":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen";
 | 
			
		||||
		case "opus":
 | 
			
		||||
			return !!player.canPlayType("audio/ogg; codecs=\"opus\"");
 | 
			
		||||
		case "flac":
 | 
			
		||||
			return browser === "orsay" || browser === "tizen" || browser === "edge";
 | 
			
		||||
		default:
 | 
			
		||||
			return false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,211 +0,0 @@
 | 
			
		||||
<div id="root"
 | 
			
		||||
     (mouseenter)="!isMobile ? this.showControls = true : null"
 | 
			
		||||
     (mouseleave)="!this.player.paused && !isMobile ? this.showControls = this.isMenuOpen : null"
 | 
			
		||||
     [style.cursor]="this.showControls ? '' : 'none'">
 | 
			
		||||
	<div class="player data-vjs-player">
 | 
			
		||||
		<video id="player" #player
 | 
			
		||||
			   [poster]="this.item.backdrop"
 | 
			
		||||
		       autoplay muted
 | 
			
		||||
		       (click)="this.videoClicked()"
 | 
			
		||||
		       (dblclick)="this.fullscreen()"
 | 
			
		||||
		       (play)="this.playing = true; this.loading = false"
 | 
			
		||||
		       (pause)="this.playing = false"
 | 
			
		||||
		       (ended)="this.next()"
 | 
			
		||||
		       [volume]="this.volume / 100"
 | 
			
		||||
		       [muted]="this.muted"
 | 
			
		||||
		       (waiting)="this.loading = true"
 | 
			
		||||
		       (canplay)="this.loading = false"
 | 
			
		||||
		       (error)="this.playbackError()">
 | 
			
		||||
		</video>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div id="loadIndicator" [ngClass]="{'d-none': !this.loading}">
 | 
			
		||||
		<div class="spinner-border align-self-center" role="status"></div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<mat-card class="d-none w-25 m-5 stats" [ngClass]="{'d-block': this.displayStats}">
 | 
			
		||||
		<mat-card-header>
 | 
			
		||||
			<h4>Stats</h4>
 | 
			
		||||
			<div style="flex: 1 1 auto"></div>
 | 
			
		||||
			<button mat-icon-button aria-label="Close" (click)="this.displayStats = false">
 | 
			
		||||
				<mat-icon>close</mat-icon>
 | 
			
		||||
			</button>
 | 
			
		||||
		</mat-card-header>
 | 
			
		||||
		<mat-card-content>
 | 
			
		||||
			Play method: <span>{{this.playMethod}}</span>
 | 
			
		||||
			<br />
 | 
			
		||||
			<br />
 | 
			
		||||
			Video Container:
 | 
			
		||||
			<span>
 | 
			
		||||
				{{this.item.container}}
 | 
			
		||||
				<i class="material-icons">{{this.supportList | supportedButton: "container"}}</i>
 | 
			
		||||
			</span>
 | 
			
		||||
			<br />
 | 
			
		||||
			Video Codec:
 | 
			
		||||
			<span>
 | 
			
		||||
				{{this.item.video.codec}}
 | 
			
		||||
				<i class="material-icons">{{this.supportList | supportedButton: "video"}}</i>
 | 
			
		||||
			</span>
 | 
			
		||||
			<br />
 | 
			
		||||
			Audio Codec:
 | 
			
		||||
			<span>
 | 
			
		||||
				{{this.item.audios[this.selectedAudio].codec}}
 | 
			
		||||
				<i class="material-icons">{{this.supportList | supportedButton: "audio":this.selectedAudio}}</i>
 | 
			
		||||
			</span>
 | 
			
		||||
			<br />
 | 
			
		||||
			Subtitle Codec:
 | 
			
		||||
			<span>
 | 
			
		||||
				{{this.selectedSubtitle != -1 ? this.item.subtitles[this.selectedSubtitle].codec : "none"}}
 | 
			
		||||
				<i class="material-icons">{{this.supportList | supportedButton: "subtitle"}}</i>
 | 
			
		||||
			</span>
 | 
			
		||||
			<br />
 | 
			
		||||
		</mat-card-content>
 | 
			
		||||
	</mat-card>
 | 
			
		||||
 | 
			
		||||
	<div id="hover"
 | 
			
		||||
	     (mouseenter)="this.areControlHovered = true"
 | 
			
		||||
	     (mouseleave)="this.areControlHovered = false"
 | 
			
		||||
	     [ngClass]="{'idle': !this.showControls}">
 | 
			
		||||
		<div class="back">
 | 
			
		||||
			<a mat-icon-button matTooltipPosition="below" matTooltip="Back" (click)="back()">
 | 
			
		||||
				<mat-icon>arrow_back</mat-icon>
 | 
			
		||||
			</a>
 | 
			
		||||
			<h5>{{this.item.showTitle}}</h5>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="controller container-fluid" id="controller">
 | 
			
		||||
			<div class="img d-none d-sm-block">
 | 
			
		||||
				<img [src]="this.item.poster" alt="poster" />
 | 
			
		||||
			</div>
 | 
			
		||||
			<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">{{this.item.title}}</h3>
 | 
			
		||||
 | 
			
		||||
				<div id="progress-bar" #progressBar
 | 
			
		||||
				     [ngClass]="{'seeking': this.seeking}"
 | 
			
		||||
				     (click)="player.currentTime = this.getTimeFromSeekbar($event.pageX);"
 | 
			
		||||
				     (mousedown)="this.startSeeking($event)"
 | 
			
		||||
				     (touchstart)="this.startSeeking($event)">
 | 
			
		||||
					<div class="seek-bar">
 | 
			
		||||
						<div id="buffered" [style.width]="player.buffered | bufferToWidth: player.duration">
 | 
			
		||||
						</div>
 | 
			
		||||
						<div id="progress" [style.width]="(player.currentTime / player.duration * 100) + '%'"></div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div id="thumb" [style.transform]="'translateX(' + (player.currentTime / player.duration * 100) + '%)'">
 | 
			
		||||
						<div></div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="buttons">
 | 
			
		||||
					<div class="left">
 | 
			
		||||
						<a *ngIf="this.item.previousEpisode" mat-icon-button matTooltipPosition="above" matTooltip="Previous"
 | 
			
		||||
						   (click)="previous()">
 | 
			
		||||
							<mat-icon>skip_previous</mat-icon>
 | 
			
		||||
						</a>
 | 
			
		||||
						<button mat-icon-button matTooltipPosition="above" id="play"
 | 
			
		||||
						        [matTooltip]="this.playing ? 'Pause' : 'Play'"
 | 
			
		||||
						        (click)="togglePlayback()">
 | 
			
		||||
							<mat-icon>{{this.playing ? 'pause' : 'play_arrow'}}</mat-icon>
 | 
			
		||||
						</button>
 | 
			
		||||
						<a mat-icon-button id="nextBtn" *ngIf="this.item.nextEpisode" (click)="next()">
 | 
			
		||||
							<mat-icon>skip_next</mat-icon>
 | 
			
		||||
 | 
			
		||||
							<div id="next">
 | 
			
		||||
								<div id="main">
 | 
			
		||||
									<img src="{{this.item.nextEpisode.thumb}}" alt="next episode thumbnail" />
 | 
			
		||||
								</div>
 | 
			
		||||
								<div id="overview">
 | 
			
		||||
									<h6>S{{this.item.nextEpisode.seasonNumber}}:E{{this.item.nextEpisode.episodeNumber}} - {{this.item.nextEpisode.title}}</h6>
 | 
			
		||||
									<p>{{this.item.nextEpisode.overview}}</p>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</a>
 | 
			
		||||
						<div id="volume" [ngClass]="{'d-none': this.isMobile}">
 | 
			
		||||
							<button mat-icon-button matTooltipPosition="above" matTooltip="Volume"
 | 
			
		||||
							        (click)="this.muted = !this.muted">
 | 
			
		||||
								<mat-icon>{{this.volume | volumeToButton: this.muted}}</mat-icon>
 | 
			
		||||
							</button>
 | 
			
		||||
 | 
			
		||||
							<mat-slider [value]="this.volume" (input)="this.volume = $event.value"></mat-slider>
 | 
			
		||||
						</div>
 | 
			
		||||
						<p>{{player.currentTime | formatTime: player.duration}} / {{player.duration | formatTime}}</p>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="right">
 | 
			
		||||
						<button *ngIf="this.item.audios.length > 1" mat-icon-button
 | 
			
		||||
						        matTooltipPosition="above" matTooltip="Select audio track">
 | 
			
		||||
							<mat-icon>music_note</mat-icon>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button *ngIf="this.item.subtitles.length > 0" mat-icon-button [matMenuTriggerFor]="subtitles"
 | 
			
		||||
						        (menuOpened)="this.isMenuOpen = true"
 | 
			
		||||
						        matTooltipPosition="above" matTooltip="Select subtitle track">
 | 
			
		||||
							<mat-icon>closed_caption</mat-icon>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button mat-icon-button matTooltipPosition="above" matTooltip="Cast">
 | 
			
		||||
							<mat-icon>cast</mat-icon>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button mat-icon-button matTooltipPosition="above" matTooltip="Settings"
 | 
			
		||||
						        (menuOpened)="this.isMenuOpen = true"
 | 
			
		||||
						        [matMenuTriggerFor]="settings">
 | 
			
		||||
							<mat-icon>settings</mat-icon>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button mat-icon-button matTooltipPosition="above"
 | 
			
		||||
						        [ngClass]="{'d-none': this.isMobile}"
 | 
			
		||||
						        [matTooltip]="this.isFullScreen ? 'Exit fullscreen' : 'Fullscreen'"
 | 
			
		||||
						        (click)="fullscreen()">
 | 
			
		||||
							<mat-icon>{{this.isFullScreen ? "fullscreen_exit" : "fullscreen"}}</mat-icon>
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<mat-menu #subtitles="matMenu" (closed)="this.isMenuOpen = false">
 | 
			
		||||
				<ng-template matMenuContent>
 | 
			
		||||
					<button [ngClass]="{'selected': this.selectedSubtitle === -1}" mat-menu-item
 | 
			
		||||
					        (click)="selectSubtitle(null)">
 | 
			
		||||
						<span>None</span>
 | 
			
		||||
					</button>
 | 
			
		||||
 | 
			
		||||
					<div *ngFor="let subtitle of this.item.subtitles; index as i">
 | 
			
		||||
						<button [ngClass]="{'selected': this.selectedSubtitle === i}"
 | 
			
		||||
						        mat-menu-item *ngIf="subtitle.codec === 'ass' || subtitle.codec === 'subrip';
 | 
			
		||||
						                             else elseBlock"
 | 
			
		||||
						        (click)="selectSubtitle(subtitle)">
 | 
			
		||||
							<span>{{subtitle.displayName}}</span>
 | 
			
		||||
						</button>
 | 
			
		||||
 | 
			
		||||
						<ng-template #elseBlock>
 | 
			
		||||
							<button mat-menu-item disabled>
 | 
			
		||||
								<span>{{subtitle.displayName}} ({{subtitle.codec}})</span>
 | 
			
		||||
							</button>
 | 
			
		||||
						</ng-template>
 | 
			
		||||
					</div>
 | 
			
		||||
				</ng-template>
 | 
			
		||||
			</mat-menu>
 | 
			
		||||
 | 
			
		||||
			<mat-menu #settings="matMenu" (closed)="this.isMenuOpen = false">
 | 
			
		||||
				<ng-template matMenuContent>
 | 
			
		||||
					<button mat-menu-item (click)="this.displayStats = !this.displayStats">
 | 
			
		||||
						<span>Stats</span>
 | 
			
		||||
					</button>
 | 
			
		||||
					<button mat-menu-item [matMenuTriggerFor]="method">
 | 
			
		||||
						<span>Method</span>
 | 
			
		||||
					</button>
 | 
			
		||||
				</ng-template>
 | 
			
		||||
			</mat-menu>
 | 
			
		||||
 | 
			
		||||
			<mat-menu #method="matMenu">
 | 
			
		||||
				<ng-template matMenuContent>
 | 
			
		||||
					<button mat-menu-item (click)="selectPlayMethod(methodType.direct)">
 | 
			
		||||
						<span>Direct Play</span>
 | 
			
		||||
					</button>
 | 
			
		||||
					<button mat-menu-item (click)="selectPlayMethod(methodType.transmux)">
 | 
			
		||||
						<span>Transmux</span>
 | 
			
		||||
					</button>
 | 
			
		||||
					<button mat-menu-item (click)="selectPlayMethod(methodType.transcode)">
 | 
			
		||||
						<span>Transcode</span>
 | 
			
		||||
					</button>
 | 
			
		||||
				</ng-template>
 | 
			
		||||
			</mat-menu>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,365 +0,0 @@
 | 
			
		||||
@import "vtt-subtitles";
 | 
			
		||||
 | 
			
		||||
.player
 | 
			
		||||
{
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	background: #000;
 | 
			
		||||
 | 
			
		||||
	> video
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
		object-fit: contain;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#hover
 | 
			
		||||
{
 | 
			
		||||
	transition: opacity .2s linear;
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	visibility: visible;
 | 
			
		||||
 | 
			
		||||
	&.idle
 | 
			
		||||
	{
 | 
			
		||||
		transition: opacity .6s linear, visibility 0s .6s;
 | 
			
		||||
		opacity: 0;
 | 
			
		||||
		visibility: hidden;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back
 | 
			
		||||
{
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	background: rgba(0, 0, 0, 0.6);
 | 
			
		||||
	padding: .33%;
 | 
			
		||||
	display: flex;
 | 
			
		||||
 | 
			
		||||
	> a
 | 
			
		||||
	{
 | 
			
		||||
		outline: none;
 | 
			
		||||
		color: inherit;
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> h5
 | 
			
		||||
	{
 | 
			
		||||
		margin: 0 0 0 .5rem;
 | 
			
		||||
		align-self: center;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.controller
 | 
			
		||||
{
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	background: rgba(0, 0, 0, 0.6);
 | 
			
		||||
	display: flex;
 | 
			
		||||
	padding: 1%;
 | 
			
		||||
 | 
			
		||||
	.img
 | 
			
		||||
	{
 | 
			
		||||
		width: 15%;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		height: auto;
 | 
			
		||||
 | 
			
		||||
		> img
 | 
			
		||||
		{
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			height: auto;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		margin-left: 1rem;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
 | 
			
		||||
		.buttons
 | 
			
		||||
		{
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
			> div
 | 
			
		||||
			{
 | 
			
		||||
				&.left
 | 
			
		||||
				{
 | 
			
		||||
					align-self: start;
 | 
			
		||||
					display: flex;
 | 
			
		||||
 | 
			
		||||
					> p
 | 
			
		||||
					{
 | 
			
		||||
						margin: 0 0 0 1rem;
 | 
			
		||||
						align-self: center;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.right
 | 
			
		||||
				{
 | 
			
		||||
					align-self: end;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> button
 | 
			
		||||
				{
 | 
			
		||||
					margin-left: .3rem;
 | 
			
		||||
					margin-right: .3rem;
 | 
			
		||||
					outline: none;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> a
 | 
			
		||||
				{
 | 
			
		||||
					margin-left: .3rem;
 | 
			
		||||
					margin-right: .3rem;
 | 
			
		||||
					outline: none;
 | 
			
		||||
					color: inherit;
 | 
			
		||||
					text-decoration: inherit;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#progress-bar
 | 
			
		||||
{
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	height: auto;
 | 
			
		||||
	padding-top: 1rem;
 | 
			
		||||
	padding-bottom: 1rem;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	.seek-bar
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 4px;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		background-color: rgba(255, 255, 255, .2);
 | 
			
		||||
		transform: scaleY(.6);
 | 
			
		||||
 | 
			
		||||
		#progress
 | 
			
		||||
		{
 | 
			
		||||
			width: 0;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			background-color: var(--accentColor);
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		#buffered
 | 
			
		||||
		{
 | 
			
		||||
			width: 0;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			background-color: rgba(255, 255, 255, .5);
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	#thumb
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 12px;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		left: -6px;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		margin: auto;
 | 
			
		||||
		opacity: 0;
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
		{
 | 
			
		||||
			width: 12px;
 | 
			
		||||
			height: 12px;
 | 
			
		||||
			border-radius: 6px;
 | 
			
		||||
			background-color: var(--accentColor);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.hoverEnabled &:hover, &.seeking
 | 
			
		||||
	{
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
 | 
			
		||||
		.seek-bar
 | 
			
		||||
		{
 | 
			
		||||
			transform: scaleY(1);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		#thumb
 | 
			
		||||
		{
 | 
			
		||||
			opacity: 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#nextBtn
 | 
			
		||||
{
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	.hoverEnabled &:hover
 | 
			
		||||
	{
 | 
			
		||||
		#next
 | 
			
		||||
		{
 | 
			
		||||
			display: flex;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	#next
 | 
			
		||||
	{
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		bottom: 100%;
 | 
			
		||||
		display: none;
 | 
			
		||||
		background-color: #212121;
 | 
			
		||||
		white-space: normal;
 | 
			
		||||
		line-height: normal;
 | 
			
		||||
		cursor: default;
 | 
			
		||||
		height: 150px;
 | 
			
		||||
 | 
			
		||||
		#main
 | 
			
		||||
		{
 | 
			
		||||
			width: auto;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			flex-shrink: 0;
 | 
			
		||||
			flex-grow: 0;
 | 
			
		||||
 | 
			
		||||
			> img
 | 
			
		||||
			{
 | 
			
		||||
				width: auto;
 | 
			
		||||
				height: 100%;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		#overview
 | 
			
		||||
		{
 | 
			
		||||
			padding: 1%;
 | 
			
		||||
			width: 50%;
 | 
			
		||||
			min-width: 300px;
 | 
			
		||||
			flex-shrink: 0;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-direction: column;
 | 
			
		||||
 | 
			
		||||
			> p
 | 
			
		||||
			{
 | 
			
		||||
				text-align: justify;
 | 
			
		||||
				font-weight: 300;
 | 
			
		||||
				overflow: hidden;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#volume
 | 
			
		||||
{
 | 
			
		||||
	display: flex;
 | 
			
		||||
 | 
			
		||||
	> button
 | 
			
		||||
	{
 | 
			
		||||
		outline: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.hoverEnabled &:hover, &:focus-within
 | 
			
		||||
	{
 | 
			
		||||
		> mat-slider
 | 
			
		||||
		{
 | 
			
		||||
			width: 100px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> mat-slider
 | 
			
		||||
	{
 | 
			
		||||
		width: 0;
 | 
			
		||||
		min-width: 0;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		height: 40px;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		transition: width .2s cubic-bezier(0.4,0, 1, 1);
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
		{
 | 
			
		||||
			top: 19px;
 | 
			
		||||
			left: 10px;
 | 
			
		||||
			right: 10px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mat-menu-item
 | 
			
		||||
{
 | 
			
		||||
	outline: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selected
 | 
			
		||||
{
 | 
			
		||||
	background: #595959 !important;
 | 
			
		||||
	color: var(--accentColor);
 | 
			
		||||
	font-weight: 900;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#loadIndicator
 | 
			
		||||
{
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	background: rgba(0, 0, 0, 0.3);
 | 
			
		||||
	display: flex;
 | 
			
		||||
	justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.volume
 | 
			
		||||
{
 | 
			
		||||
	min-width: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.info-panel
 | 
			
		||||
{
 | 
			
		||||
	min-width: 250px !important;
 | 
			
		||||
	max-width: 300px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stats
 | 
			
		||||
{
 | 
			
		||||
	> mat-card-header
 | 
			
		||||
	{
 | 
			
		||||
		margin-bottom: 0.5rem;
 | 
			
		||||
 | 
			
		||||
		> h4
 | 
			
		||||
		{
 | 
			
		||||
			align-self: center;
 | 
			
		||||
			margin-bottom: 0;
 | 
			
		||||
		}
 | 
			
		||||
		> button
 | 
			
		||||
		{
 | 
			
		||||
			outline: none;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	> mat-card-content > span
 | 
			
		||||
	{
 | 
			
		||||
		float: right;
 | 
			
		||||
		> i
 | 
			
		||||
		{
 | 
			
		||||
			vertical-align: middle;
 | 
			
		||||
			font-size: 14px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,591 +0,0 @@
 | 
			
		||||
import { Location } from "@angular/common";
 | 
			
		||||
import {
 | 
			
		||||
	AfterViewInit,
 | 
			
		||||
	Component, ElementRef, HostListener,
 | 
			
		||||
	Injector,
 | 
			
		||||
	OnDestroy,
 | 
			
		||||
	OnInit,
 | 
			
		||||
	Pipe,
 | 
			
		||||
	PipeTransform,
 | 
			
		||||
	ViewChild,
 | 
			
		||||
	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 { OidcSecurityService } from "angular-auth-oidc-client";
 | 
			
		||||
import * as Hls from "hls.js";
 | 
			
		||||
import { ShowService } from "../../services/api.service";
 | 
			
		||||
import { StartupService } from "../../services/startup.service";
 | 
			
		||||
import {
 | 
			
		||||
	getWhatIsSupported,
 | 
			
		||||
	method,
 | 
			
		||||
	SupportList
 | 
			
		||||
} from "./playbackMethodDetector";
 | 
			
		||||
import { AppComponent } from "../../app.component";
 | 
			
		||||
import { Track, WatchItem } from "../../models/watch-item";
 | 
			
		||||
import SubtitlesOctopus from "libass-wasm/dist/js/subtitles-octopus.js";
 | 
			
		||||
import MouseMoveEvent = JQuery.MouseMoveEvent;
 | 
			
		||||
import TouchMoveEvent = JQuery.TouchMoveEvent;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
	name: "formatTime",
 | 
			
		||||
	pure: true
 | 
			
		||||
})
 | 
			
		||||
export class FormatTimePipe implements PipeTransform
 | 
			
		||||
{
 | 
			
		||||
	transform(value: number, hourCheck: number = null): string
 | 
			
		||||
	{
 | 
			
		||||
		if (isNaN(value) || value === null || value === undefined)
 | 
			
		||||
			return `??:??`;
 | 
			
		||||
		hourCheck ??= value;
 | 
			
		||||
		if (hourCheck >= 3600)
 | 
			
		||||
			return new Date(value * 1000).toISOString().substr(11, 8);
 | 
			
		||||
		return new Date(value * 1000).toISOString().substr(14, 5);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
	name: "bufferToWidth",
 | 
			
		||||
	pure: true
 | 
			
		||||
})
 | 
			
		||||
export class BufferToWidthPipe implements PipeTransform
 | 
			
		||||
{
 | 
			
		||||
	transform(buffered: TimeRanges, duration: number): string
 | 
			
		||||
	{
 | 
			
		||||
		if (buffered.length === 0)
 | 
			
		||||
			return "0";
 | 
			
		||||
		return `${buffered.end(buffered.length - 1) / duration * 100}%`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
	name: "volumeToButton",
 | 
			
		||||
	pure: true
 | 
			
		||||
})
 | 
			
		||||
export class VolumeToButtonPipe implements PipeTransform
 | 
			
		||||
{
 | 
			
		||||
	transform(volume: number, muted: boolean): string
 | 
			
		||||
	{
 | 
			
		||||
		if (volume === 0 || muted)
 | 
			
		||||
			return "volume_off";
 | 
			
		||||
		else if (volume < 25)
 | 
			
		||||
			return "volume_mute";
 | 
			
		||||
		else if (volume < 65)
 | 
			
		||||
			return "volume_down";
 | 
			
		||||
		else
 | 
			
		||||
			return "volume_up";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
	name: "supportedButton",
 | 
			
		||||
	pure: true
 | 
			
		||||
})
 | 
			
		||||
export class SupportedButtonPipe implements PipeTransform
 | 
			
		||||
{
 | 
			
		||||
	transform(supports: SupportList, selector: string, audioIndex: number = 0): string
 | 
			
		||||
	{
 | 
			
		||||
		if (!supports)
 | 
			
		||||
			return "help";
 | 
			
		||||
		switch (selector)
 | 
			
		||||
		{
 | 
			
		||||
			case "container":
 | 
			
		||||
				return supports.container ? "check_circle" : "cancel";
 | 
			
		||||
			case "video":
 | 
			
		||||
				return supports.videoCodec ? "check_circle" : "cancel";
 | 
			
		||||
			case "audio":
 | 
			
		||||
				return supports.audioCodec[audioIndex] ? "check_circle" : "cancel";
 | 
			
		||||
			default:
 | 
			
		||||
				return "help";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-player",
 | 
			
		||||
	templateUrl: "./player.component.html",
 | 
			
		||||
	styleUrls: ["./player.component.scss"],
 | 
			
		||||
	encapsulation: ViewEncapsulation.None
 | 
			
		||||
})
 | 
			
		||||
export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
 | 
			
		||||
{
 | 
			
		||||
	item: WatchItem;
 | 
			
		||||
	selectedAudio: number = 0;
 | 
			
		||||
	selectedSubtitle: number = -1;
 | 
			
		||||
	playMethod: method = method.direct;
 | 
			
		||||
	supportList: SupportList;
 | 
			
		||||
	playing: boolean = true;
 | 
			
		||||
	loading: boolean = false;
 | 
			
		||||
	seeking: boolean = false;
 | 
			
		||||
	muted: boolean = false;
 | 
			
		||||
 | 
			
		||||
	private _volume: number = 100;
 | 
			
		||||
	get volume(): number { return this._volume; }
 | 
			
		||||
	set volume(value: number) { this._volume = Math.max(0, Math.min(value, 100)); }
 | 
			
		||||
 | 
			
		||||
	@ViewChild("player") private playerRef: ElementRef;
 | 
			
		||||
	private get player(): HTMLVideoElement { return this.playerRef.nativeElement; }
 | 
			
		||||
	@ViewChild("progressBar") private progressBarRef: ElementRef;
 | 
			
		||||
	private get progressBar(): HTMLElement { return this.progressBarRef.nativeElement; }
 | 
			
		||||
 | 
			
		||||
	controlHider: NodeJS.Timeout = null;
 | 
			
		||||
	areControlHovered: boolean = false;
 | 
			
		||||
	isMenuOpen: boolean = false;
 | 
			
		||||
	private _showControls: boolean = true;
 | 
			
		||||
	get showControls(): boolean { return this._showControls; }
 | 
			
		||||
	set showControls(value: boolean)
 | 
			
		||||
	{
 | 
			
		||||
		this._showControls = value;
 | 
			
		||||
		if (this.controlHider)
 | 
			
		||||
			clearTimeout(this.controlHider);
 | 
			
		||||
		if (value)
 | 
			
		||||
		{
 | 
			
		||||
			this.controlHider = setTimeout(() =>
 | 
			
		||||
			{
 | 
			
		||||
				this.showControls = this.player.paused || this.areControlHovered || this.isMenuOpen;
 | 
			
		||||
			}, 2500);
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
			this.controlHider = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	methodType = method;
 | 
			
		||||
	displayStats: boolean = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private subtitlesManager: SubtitlesOctopus;
 | 
			
		||||
	private hlsPlayer: Hls = new Hls();
 | 
			
		||||
	private oidcSecurity: OidcSecurityService;
 | 
			
		||||
	constructor(private route: ActivatedRoute,
 | 
			
		||||
	            private sanitizer: DomSanitizer,
 | 
			
		||||
	            private snackBar: MatSnackBar,
 | 
			
		||||
	            private title: Title,
 | 
			
		||||
	            private router: Router,
 | 
			
		||||
	            private location: Location,
 | 
			
		||||
	            private injector: Injector,
 | 
			
		||||
	            private shows: ShowService,
 | 
			
		||||
	            private startup: StartupService)
 | 
			
		||||
	{ }
 | 
			
		||||
 | 
			
		||||
	ngOnInit(): void
 | 
			
		||||
	{
 | 
			
		||||
		document.getElementById("nav").classList.add("d-none");
 | 
			
		||||
		if (AppComponent.isMobile)
 | 
			
		||||
		{
 | 
			
		||||
			if (!this.isFullScreen)
 | 
			
		||||
				this.fullscreen();
 | 
			
		||||
			screen.orientation.lock("landscape");
 | 
			
		||||
			$(document).on("fullscreenchange", () =>
 | 
			
		||||
			{
 | 
			
		||||
				if (document.fullscreenElement == null && this.router.url.startsWith("/watch"))
 | 
			
		||||
					this.back();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.route.data.subscribe(data =>
 | 
			
		||||
		{
 | 
			
		||||
			this.item = data.item;
 | 
			
		||||
 | 
			
		||||
			const name: string = this.item.isMovie
 | 
			
		||||
				? this.item.showTitle
 | 
			
		||||
				: `${this.item.showTitle} S${this.item.seasonNumber}:E${this.item.episodeNumber}`;
 | 
			
		||||
 | 
			
		||||
			if (this.item.isMovie)
 | 
			
		||||
				this.title.setTitle(`${name} - Kyoo`);
 | 
			
		||||
			else
 | 
			
		||||
				this.title.setTitle(`${name} - Kyoo`);
 | 
			
		||||
 | 
			
		||||
			setTimeout(() =>
 | 
			
		||||
			{
 | 
			
		||||
				this.snackBar.open(`Playing: ${name}`, null, {
 | 
			
		||||
					verticalPosition: "top",
 | 
			
		||||
					horizontalPosition: "right",
 | 
			
		||||
					duration: 2000,
 | 
			
		||||
					panelClass: "info-panel"
 | 
			
		||||
				});
 | 
			
		||||
			}, 750);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.router.events.subscribe((event: Event) =>
 | 
			
		||||
		{
 | 
			
		||||
			switch (true)
 | 
			
		||||
			{
 | 
			
		||||
				case event instanceof NavigationStart:
 | 
			
		||||
					this.loading = true;
 | 
			
		||||
					break;
 | 
			
		||||
				case event instanceof NavigationEnd:
 | 
			
		||||
				case event instanceof NavigationCancel:
 | 
			
		||||
					this.loading = false;
 | 
			
		||||
					break;
 | 
			
		||||
				default:
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ngOnDestroy(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.subtitlesManager)
 | 
			
		||||
			this.subtitlesManager.dispose();
 | 
			
		||||
		if (this.isFullScreen)
 | 
			
		||||
			document.exitFullscreen();
 | 
			
		||||
 | 
			
		||||
		document.getElementById("nav").classList.remove("d-none");
 | 
			
		||||
		this.title.setTitle("Kyoo");
 | 
			
		||||
 | 
			
		||||
		$(document).off();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ngAfterViewInit(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.oidcSecurity === undefined)
 | 
			
		||||
			this.oidcSecurity = this.injector.get(OidcSecurityService);
 | 
			
		||||
		this.hlsPlayer.config.xhrSetup = xhr =>
 | 
			
		||||
		{
 | 
			
		||||
			const token: string = this.oidcSecurity.getToken();
 | 
			
		||||
			if (token)
 | 
			
		||||
				xhr.setRequestHeader("Authorization", "Bearer " + token);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		this.showControls = true;
 | 
			
		||||
 | 
			
		||||
		setTimeout(() => this.route.data.subscribe(() =>
 | 
			
		||||
		{
 | 
			
		||||
			// TODO remove the query param for the method (should be a session setting).
 | 
			
		||||
			const queryMethod: string = this.route.snapshot.queryParams.method;
 | 
			
		||||
			this.supportList = getWhatIsSupported(this.player, this.item);
 | 
			
		||||
			this.selectPlayMethod(queryMethod ? method[queryMethod] : this.supportList.getPlaybackMethod());
 | 
			
		||||
 | 
			
		||||
			// TODO remove this, it should be a user's setting.
 | 
			
		||||
			const subSlug: string = this.route.snapshot.queryParams.sub;
 | 
			
		||||
			if (subSlug != null)
 | 
			
		||||
			{
 | 
			
		||||
				const languageCode: string = subSlug.substring(0, 3);
 | 
			
		||||
				const forced: boolean = subSlug.length > 3 && subSlug.substring(4) === "for";
 | 
			
		||||
				const sub: Track = this.item.subtitles.find(x => x.language === languageCode && x.isForced === forced);
 | 
			
		||||
				this.selectSubtitle(sub, false);
 | 
			
		||||
			}
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get isFullScreen(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return document.fullscreenElement != null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get isMobile(): boolean
 | 
			
		||||
	{
 | 
			
		||||
		return AppComponent.isMobile;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getTimeFromSeekbar(pageX: number): number
 | 
			
		||||
	{
 | 
			
		||||
		const value: number = (pageX - this.progressBar.offsetLeft) / this.progressBar.clientWidth;
 | 
			
		||||
		const percent: number = Math.max(0, Math.min(value, 1));
 | 
			
		||||
		return percent * this.player.duration;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	startSeeking(event: MouseEvent | TouchEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
		this.seeking = true;
 | 
			
		||||
		this.player.pause();
 | 
			
		||||
		const pageX: number = "pageX" in event ? event.pageX : event.changedTouches[0].pageX;
 | 
			
		||||
		this.player.currentTime = this.getTimeFromSeekbar(pageX);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("document:mouseup", ["$event"])
 | 
			
		||||
	@HostListener("document:touchend", ["$event"])
 | 
			
		||||
	endSeeking(event: MouseEvent | TouchEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		if (!this.seeking)
 | 
			
		||||
			return;
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
		this.seeking = false;
 | 
			
		||||
		const pageX: number = "pageX" in event ? event.pageX : event.changedTouches[0].pageX;
 | 
			
		||||
		this.player.currentTime = this.getTimeFromSeekbar(pageX);
 | 
			
		||||
		this.player.play();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("document:touchmove", ["$event"])
 | 
			
		||||
	touchSeek(event: TouchMoveEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.seeking)
 | 
			
		||||
			this.player.currentTime = this.getTimeFromSeekbar(event.changedTouches[0].pageX);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("document:mousemove", ["$event"])
 | 
			
		||||
	mouseMove(event: MouseMoveEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.seeking)
 | 
			
		||||
			this.player.currentTime = this.getTimeFromSeekbar(event.pageX);
 | 
			
		||||
		else if (!AppComponent.isMobile)
 | 
			
		||||
			this.showControls = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	playbackError(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.playMethod === method.transcode)
 | 
			
		||||
		{
 | 
			
		||||
			this.snackBar.open("This episode can't be played.", null, {
 | 
			
		||||
				horizontalPosition: "left",
 | 
			
		||||
				panelClass: ["snackError"],
 | 
			
		||||
				duration: 10000
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			if (this.playMethod === method.direct)
 | 
			
		||||
				this.playMethod = method.transmux;
 | 
			
		||||
			else
 | 
			
		||||
				this.playMethod = method.transcode;
 | 
			
		||||
			this.selectPlayMethod(this.playMethod);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectPlayMethod(playMethod: method): void
 | 
			
		||||
	{
 | 
			
		||||
		this.playMethod = playMethod;
 | 
			
		||||
		const url: string = [
 | 
			
		||||
			"/video",
 | 
			
		||||
			this.playMethod.toLowerCase(),
 | 
			
		||||
			this.item.slug,
 | 
			
		||||
			this.playMethod !== method.direct ? "master.m3u8" : null
 | 
			
		||||
		].filter(x => x !== null).join("/");
 | 
			
		||||
		if (this.playMethod === method.direct || this.player.canPlayType("application/vnd.apple.mpegurl"))
 | 
			
		||||
			this.player.src = url;
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			this.hlsPlayer.loadSource(url);
 | 
			
		||||
			this.hlsPlayer.attachMedia(this.player);
 | 
			
		||||
			this.hlsPlayer.on(Hls.Events.MANIFEST_LOADED, () =>
 | 
			
		||||
			{
 | 
			
		||||
				this.player.play();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	back(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.startup.loadedFromWatch)
 | 
			
		||||
		{
 | 
			
		||||
			this.router.navigate(["show", this.startup.show], {replaceUrl: true});
 | 
			
		||||
			this.startup.loadedFromWatch = false;
 | 
			
		||||
			this.startup.show = null;
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
			this.location.back();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	next(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.item.nextEpisode == null)
 | 
			
		||||
			return;
 | 
			
		||||
		this.router.navigate(["/watch", this.item.nextEpisode.slug], {
 | 
			
		||||
			queryParamsHandling: "merge",
 | 
			
		||||
			replaceUrl: true
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	previous(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.item.previousEpisode == null)
 | 
			
		||||
			return;
 | 
			
		||||
		this.router.navigate(["/watch", this.item.previousEpisode.slug], {
 | 
			
		||||
			queryParamsHandling: "merge",
 | 
			
		||||
			replaceUrl: true
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	videoClicked(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (AppComponent.isMobile)
 | 
			
		||||
			this.showControls = !this.showControls;
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			this.showControls = !this.player.paused;
 | 
			
		||||
			this.togglePlayback();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	togglePlayback(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.player.paused)
 | 
			
		||||
			this.player.play();
 | 
			
		||||
		else
 | 
			
		||||
			this.player.pause();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fullscreen(): void
 | 
			
		||||
	{
 | 
			
		||||
		if (this.isFullScreen)
 | 
			
		||||
			document.exitFullscreen();
 | 
			
		||||
		else
 | 
			
		||||
			document.body.requestFullscreen();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async selectSubtitle(subtitle: Track | number, changeUrl: boolean = true): Promise<void>
 | 
			
		||||
	{
 | 
			
		||||
		if (typeof(subtitle) === "number")
 | 
			
		||||
		{
 | 
			
		||||
			this.selectedSubtitle = subtitle;
 | 
			
		||||
			subtitle = this.item.subtitles[subtitle];
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
			this.selectedSubtitle = this.item.subtitles.indexOf(subtitle);
 | 
			
		||||
 | 
			
		||||
		if (changeUrl)
 | 
			
		||||
		{
 | 
			
		||||
			let subSlug: string;
 | 
			
		||||
			if (subtitle != null)
 | 
			
		||||
			{
 | 
			
		||||
				subSlug = subtitle.language;
 | 
			
		||||
				if (subtitle.isForced)
 | 
			
		||||
					subSlug += "-for";
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.router.navigate([], {
 | 
			
		||||
				relativeTo: this.route,
 | 
			
		||||
				queryParams: {sub: subSlug},
 | 
			
		||||
				replaceUrl: true,
 | 
			
		||||
				queryParamsHandling: "merge",
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		if (subtitle == null)
 | 
			
		||||
		{
 | 
			
		||||
			this.snackBar.open("Subtitle removed.", null, {
 | 
			
		||||
				verticalPosition: "top",
 | 
			
		||||
				horizontalPosition: "right",
 | 
			
		||||
				duration: 750,
 | 
			
		||||
				panelClass: "info-panel"
 | 
			
		||||
			});
 | 
			
		||||
			if (this.subtitlesManager)
 | 
			
		||||
				this.subtitlesManager.freeTrack();
 | 
			
		||||
			this.removeHtmlTrack();
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			this.snackBar.open(`${subtitle.displayName} subtitle loaded.`, null, {
 | 
			
		||||
				verticalPosition: "top",
 | 
			
		||||
				horizontalPosition: "right",
 | 
			
		||||
				duration: 750,
 | 
			
		||||
				panelClass: "info-panel"
 | 
			
		||||
			});
 | 
			
		||||
			this.removeHtmlTrack();
 | 
			
		||||
 | 
			
		||||
			if (subtitle.codec === "ass")
 | 
			
		||||
			{
 | 
			
		||||
				if (!this.subtitlesManager)
 | 
			
		||||
				{
 | 
			
		||||
					const fonts: { [key: string]: string } = await this.shows.getFonts(this.item.showSlug).toPromise();
 | 
			
		||||
					this.subtitlesManager = new SubtitlesOctopus({
 | 
			
		||||
						video: this.player,
 | 
			
		||||
						subUrl: `subtitle/${subtitle.slug}`,
 | 
			
		||||
						fonts: Object.values(fonts),
 | 
			
		||||
						renderMode: "fast"
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
					this.subtitlesManager.setTrackByUrl(`subtitle/${subtitle.slug}`);
 | 
			
		||||
			}
 | 
			
		||||
			else if (subtitle.codec === "subrip")
 | 
			
		||||
			{
 | 
			
		||||
				if (this.subtitlesManager)
 | 
			
		||||
					this.subtitlesManager.freeTrack();
 | 
			
		||||
 | 
			
		||||
				const track: HTMLTrackElement = document.createElement("track");
 | 
			
		||||
				track.kind = "subtitles";
 | 
			
		||||
				track.label = subtitle.displayName;
 | 
			
		||||
				track.srclang = subtitle.language;
 | 
			
		||||
				track.src = `subtitle/${subtitle.slug.replace(".srt", ".vtt")}`;
 | 
			
		||||
				track.classList.add("subtitle_container");
 | 
			
		||||
				track.default = true;
 | 
			
		||||
				track.onload = () =>
 | 
			
		||||
				{
 | 
			
		||||
					this.player.textTracks[0].mode = "showing";
 | 
			
		||||
				};
 | 
			
		||||
				this.player.appendChild(track);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	removeHtmlTrack(): void
 | 
			
		||||
	{
 | 
			
		||||
		const elements: HTMLCollectionOf<HTMLTrackElement> = this.player.getElementsByTagName("track");
 | 
			
		||||
		if (elements.length > 0)
 | 
			
		||||
			elements.item(0).remove();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@HostListener("document:keyup", ["$event"])
 | 
			
		||||
	keypress(event: KeyboardEvent): void
 | 
			
		||||
	{
 | 
			
		||||
		if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		switch (event.key)
 | 
			
		||||
		{
 | 
			
		||||
			case " ":
 | 
			
		||||
			case "k":
 | 
			
		||||
			case "K":
 | 
			
		||||
				this.togglePlayback();
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case "ArrowUp":
 | 
			
		||||
				this.volume += 5;
 | 
			
		||||
				this.snackBar.open(`${this.volume}%`, null, {
 | 
			
		||||
					verticalPosition: "top",
 | 
			
		||||
					horizontalPosition: "right",
 | 
			
		||||
					duration: 300,
 | 
			
		||||
					panelClass: "volume"
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			case "ArrowDown":
 | 
			
		||||
				this.volume += 5;
 | 
			
		||||
				this.snackBar.open(`${this.volume}%`, null, {
 | 
			
		||||
					verticalPosition: "top",
 | 
			
		||||
					horizontalPosition: "right",
 | 
			
		||||
					duration: 300,
 | 
			
		||||
					panelClass: "volume"
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case "v":
 | 
			
		||||
			case "V":
 | 
			
		||||
				this.selectSubtitle((this.selectedSubtitle + 2) % (this.item.subtitles.length + 1) - 1);
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case "f":
 | 
			
		||||
			case "F":
 | 
			
		||||
				this.fullscreen();
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case "m":
 | 
			
		||||
			case "M":
 | 
			
		||||
				this.muted = !this.muted;
 | 
			
		||||
				this.snackBar.open(this.player.muted ? "Sound muted." : "Sound unmuted", null, {
 | 
			
		||||
					verticalPosition: "top",
 | 
			
		||||
					horizontalPosition: "right",
 | 
			
		||||
					duration: 750,
 | 
			
		||||
					panelClass: "info-panel"
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case "n":
 | 
			
		||||
			case "N":
 | 
			
		||||
				this.next();
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case "p":
 | 
			
		||||
			case "P":
 | 
			
		||||
				this.previous();
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			default:
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
::cue
 | 
			
		||||
{
 | 
			
		||||
	background-color: transparent;
 | 
			
		||||
	text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
 | 
			
		||||
}
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
<div *ngIf="items.collections.length > 0" class="container-fluid mt-3">
 | 
			
		||||
	<h3>Collections</h3>
 | 
			
		||||
</div>
 | 
			
		||||
<app-items-list [items]="AsPage(items.collections)" type="collection"></app-items-list>
 | 
			
		||||
<div *ngIf="items.shows.length > 0" class="container-fluid mt-3">
 | 
			
		||||
	<h3>Shows</h3>
 | 
			
		||||
</div>
 | 
			
		||||
<app-items-list [items]="AsPage(items.shows)"></app-items-list>
 | 
			
		||||
<div *ngIf="items.episodes.length > 0" class="container-fluid mt-5">
 | 
			
		||||
	<h3>Episodes</h3>
 | 
			
		||||
</div>
 | 
			
		||||
<app-episodes-list displayShowTitle="true" [episodes]="AsPage(items.episodes)"></app-episodes-list>
 | 
			
		||||
<div *ngIf="items.people.length > 0" class="container-fluid mt-5">
 | 
			
		||||
	<h3>People</h3>
 | 
			
		||||
</div>
 | 
			
		||||
<app-people-list [people]="AsPage(items.people)"></app-people-list>
 | 
			
		||||
@ -1,45 +0,0 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, AfterViewInit } from "@angular/core";
 | 
			
		||||
import { ActivatedRoute } from "@angular/router";
 | 
			
		||||
import { SearchResult } from "../../models/search-result";
 | 
			
		||||
import { Title } from "@angular/platform-browser";
 | 
			
		||||
import { Page } from "../../models/page";
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
	selector: "app-search",
 | 
			
		||||
	templateUrl: "./search.component.html",
 | 
			
		||||
	styleUrls: ["./search.component.scss"]
 | 
			
		||||
})
 | 
			
		||||
export class SearchComponent implements OnInit, OnDestroy, AfterViewInit
 | 
			
		||||
{
 | 
			
		||||
	items: SearchResult;
 | 
			
		||||
 | 
			
		||||
	constructor(private route: ActivatedRoute, private title: Title) { }
 | 
			
		||||
 | 
			
		||||
	ngOnInit(): void
 | 
			
		||||
	{
 | 
			
		||||
		this.route.data.subscribe((data) =>
 | 
			
		||||
		{
 | 
			
		||||
			this.items = data.items;
 | 
			
		||||
			this.title.setTitle(this.items.query + " - Kyoo");
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ngAfterViewInit(): void
 | 
			
		||||
	{
 | 
			
		||||
		const searchBar: HTMLInputElement = document.getElementById("search") as HTMLInputElement;
 | 
			
		||||
		searchBar.classList.add("searching");
 | 
			
		||||
		searchBar.value = this.items.query;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ngOnDestroy(): void
 | 
			
		||||
	{
 | 
			
		||||
		const searchBar: HTMLInputElement = document.getElementById("search") as HTMLInputElement;
 | 
			
		||||
		searchBar.classList.remove("searching");
 | 
			
		||||
		searchBar.value = "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	AsPage<T>(collection: T[]): Page<T>
 | 
			
		||||
	{
 | 
			
		||||
		return new Page<T>({this: "", items: collection, next: null, count: collection.length});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,108 +0,0 @@
 | 
			
		||||
<div class="backdrop">
 | 
			
		||||
	<img id="backdrop" [src]="this.show.backdrop" alt="backdrop" />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="header container pt-sm-5">
 | 
			
		||||
	<div class="row">
 | 
			
		||||
		<div class="poster d-none d-sm-block">
 | 
			
		||||
			<div [style.background-image]="getThumb(this.show)"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="main col">
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<h1 class="title">{{this.show.title}}</h1>
 | 
			
		||||
				<h2 class="date" *ngIf="show.endYear && show.startYear != show.endYear; else elseBlock">{{show.startYear}} - {{show.endYear}}</h2>
 | 
			
		||||
				<ng-template #elseBlock><h2 class="date">{{show.startYear}}</h2></ng-template>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="buttons">
 | 
			
		||||
				<button mat-mini-fab matTooltipPosition="above" matTooltip="Play" class="mr-3" (click)="playClicked()">
 | 
			
		||||
					<mat-icon>play_arrow</mat-icon>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button *ngIf="this.show.trailerUrl" mat-icon-button matTooltipPosition="above" matTooltip="Trailer" (click)="openTrailer()">
 | 
			
		||||
					<mat-icon>local_movies</mat-icon>
 | 
			
		||||
				</button>
 | 
			
		||||
				<a *ngIf="this.show.isMovie" [href]="'/video/' + this.show.slug" download>
 | 
			
		||||
					<button mat-icon-button matTooltipPosition="above" matTooltip="Download">
 | 
			
		||||
						<mat-icon>cloud_download</mat-icon>
 | 
			
		||||
					</button>
 | 
			
		||||
				</a>
 | 
			
		||||
				<button mat-icon-button matTooltipPosition="above" matTooltip="Watched">
 | 
			
		||||
					<mat-icon>done</mat-icon>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button mat-icon-button matTooltipPosition="above" matTooltip="More" [matMenuTriggerFor]="showMenu">
 | 
			
		||||
					<mat-icon>more_horiz</mat-icon>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="col-3 secondary d-none d-md-block">
 | 
			
		||||
			<img [src]="this.show.logo" #logo alt="" (error)="logo.style.display = 'none'" />
 | 
			
		||||
 | 
			
		||||
			<div>
 | 
			
		||||
				<p>Studio: <b><a draggable="false"
 | 
			
		||||
				                 href="/studio/{{this.show.studio?.slug}}"
 | 
			
		||||
				                 routerLink="/studio/{{this.show.studio?.slug}}">{{this.show.studio?.name}}</a></b></p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
    <mat-menu #showMenu="matMenu">
 | 
			
		||||
        <button mat-menu-item (click)="editMetadata()">Edit metadata</button>
 | 
			
		||||
        <button mat-menu-item (click)="redownloadImages()">Re-download images</button>
 | 
			
		||||
        <button mat-menu-item (click)="extractSubs()">Re-extract subtitles</button>
 | 
			
		||||
    </mat-menu>
 | 
			
		||||
 | 
			
		||||
	<div class="row pt-3 d-md-none">
 | 
			
		||||
		<div class="col">
 | 
			
		||||
			<p class="mr-1 d-inline-block">Studio: <b><a routerLink="/studio/{{this.show.studio?.slug}}">{{this.show.studio?.name}}</a></b></p>
 | 
			
		||||
			 <div class="d-sm-none">
 | 
			
		||||
                 <p>Genres:
 | 
			
		||||
	                 <span *ngFor="let genre of this.show.genres; let isLast = last">
 | 
			
		||||
		                 <b><a draggable="false"
 | 
			
		||||
		                       href="/genre/{{genre.slug}}"
 | 
			
		||||
		                       routerLink="/genre/{{genre.slug}}">{{genre.name}}</a></b>
 | 
			
		||||
		                 {{isLast ? "" : ", "}}
 | 
			
		||||
	                 </span>
 | 
			
		||||
                 </p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="row pt-3">
 | 
			
		||||
		<div class="col">
 | 
			
		||||
			<p class="text-justify overview">{{this.show.overview}}</p>
 | 
			
		||||
            <ul>
 | 
			
		||||
                <li class="provider" *ngFor="let id of this.show.externalIDs">
 | 
			
		||||
                    <a draggable="false" [href]="id.link"><img [src]="id.provider.logo" [alt]="id.provider.name"/></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
		</div>
 | 
			
		||||
		<hr class="d-none d-sm-block">
 | 
			
		||||
		<div class="col-3 d-none d-sm-block">
 | 
			
		||||
			<h3 style="opacity: .8;">Genres</h3>
 | 
			
		||||
			<ul>
 | 
			
		||||
				<li *ngFor="let genre of this.show.genres">
 | 
			
		||||
					<b><a draggable="false" class="genre"
 | 
			
		||||
					      href="/genre/{{genre.slug}}"
 | 
			
		||||
					      routerLink="/genre/{{genre.slug}}">{{genre.name}}</a></b>
 | 
			
		||||
				</li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div *ngIf="!this.show.isMovie">
 | 
			
		||||
	<div class="container-fluid mt-3">
 | 
			
		||||
		<mat-form-field>
 | 
			
		||||
			<mat-label>Season</mat-label>
 | 
			
		||||
			<mat-select [(value)]="season" (selectionChange)="getEpisodes(season)">
 | 
			
		||||
				<mat-option *ngFor="let season of this.seasons" [value]="season.seasonNumber">{{season.title}}</mat-option>
 | 
			
		||||
			</mat-select>
 | 
			
		||||
		</mat-form-field>
 | 
			
		||||
	</div>
 | 
			
		||||
	<app-episodes-list [episodes]="episodes[season]"></app-episodes-list>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="container-fluid mt-5">
 | 
			
		||||
	<h3>Staff</h3>
 | 
			
		||||
</div>
 | 
			
		||||
<app-people-list [people]="people"></app-people-list>
 | 
			
		||||
@ -1,189 +0,0 @@
 | 
			
		||||
@import "~bootstrap/scss/functions";
 | 
			
		||||
@import "~bootstrap/scss/variables";
 | 
			
		||||
@import "~bootstrap/scss/mixins/breakpoints";
 | 
			
		||||
@import "variables";
 | 
			
		||||
 | 
			
		||||
a
 | 
			
		||||
{
 | 
			
		||||
	color: #ffffff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.backdrop
 | 
			
		||||
{
 | 
			
		||||
	margin-top: -68px;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	z-index: -1;
 | 
			
		||||
 | 
			
		||||
	> img
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		max-height: 75vh;
 | 
			
		||||
		object-fit: cover;
 | 
			
		||||
		min-height: 20vh;
 | 
			
		||||
 | 
			
		||||
		@include media-breakpoint-up(md)
 | 
			
		||||
		{
 | 
			
		||||
			min-height: 60vh;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:after
 | 
			
		||||
	{
 | 
			
		||||
		content: "";
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		right: 0;
 | 
			
		||||
		background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.6) 100%);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header
 | 
			
		||||
{
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		margin-top: -12rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		margin-top: -13rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(lg)
 | 
			
		||||
	{
 | 
			
		||||
		margin-top: -19rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(xl)
 | 
			
		||||
	{
 | 
			
		||||
		margin-top: -23rem;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.poster
 | 
			
		||||
{
 | 
			
		||||
	width: 33%;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(md)
 | 
			
		||||
	{
 | 
			
		||||
		width: 25%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		padding-top: 147.0588%;
 | 
			
		||||
		background-size: cover;
 | 
			
		||||
		background-color: #333333;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main
 | 
			
		||||
{
 | 
			
		||||
	align-self: center;
 | 
			
		||||
	padding-left: 2.5em;
 | 
			
		||||
 | 
			
		||||
	.info
 | 
			
		||||
	{
 | 
			
		||||
		margin-top: -3.25rem;
 | 
			
		||||
 | 
			
		||||
		@include media-breakpoint-up(sm)
 | 
			
		||||
		{
 | 
			
		||||
			margin-top: 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.title
 | 
			
		||||
		{
 | 
			
		||||
			font-weight: 900 !important;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.date
 | 
			
		||||
		{
 | 
			
		||||
			font-weight: 300 !important;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.buttons
 | 
			
		||||
	{
 | 
			
		||||
		> button
 | 
			
		||||
		{
 | 
			
		||||
			outline: none;
 | 
			
		||||
			margin: .3em;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.secondary
 | 
			
		||||
{
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	> img
 | 
			
		||||
	{
 | 
			
		||||
		max-width: 100%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
	{
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div > p
 | 
			
		||||
	{
 | 
			
		||||
		opacity: .87;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.overview
 | 
			
		||||
{
 | 
			
		||||
	opacity: .87;
 | 
			
		||||
 | 
			
		||||
	@include media-breakpoint-up(sm)
 | 
			
		||||
	{
 | 
			
		||||
		padding-top: 2.25rem;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
hr
 | 
			
		||||
{
 | 
			
		||||
	margin: 0 10px 0 10px;
 | 
			
		||||
	border-right: 1px solid rgba(255, 255, 255, .60);
 | 
			
		||||
	border-top: 0;
 | 
			
		||||
	height: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.genre
 | 
			
		||||
{
 | 
			
		||||
	opacity: .8;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.provider
 | 
			
		||||
{
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	width: 3rem;
 | 
			
		||||
	height: 3rem;
 | 
			
		||||
	margin: .5rem;
 | 
			
		||||
	
 | 
			
		||||
	> a
 | 
			
		||||
	{
 | 
			
		||||
		width: 3rem;
 | 
			
		||||
		height: 3rem;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
		
 | 
			
		||||
		> img
 | 
			
		||||
		{
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
			right: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			margin: auto;
 | 
			
		||||
			max-width: 3rem;
 | 
			
		||||
			max-height: 3rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user