Creating the web app player view.

This commit is contained in:
Zoe Roux 2019-09-05 04:06:49 +02:00
parent 2462d3ad7f
commit 21002fea4a
10 changed files with 281 additions and 7 deletions

View File

@ -14,7 +14,7 @@ const routes: Routes = [
{ path: "browse", component: BrowseComponent, pathMatch: "full", resolve: { shows: LibraryResolverService } },
{ path: "browse/:library-slug", component: BrowseComponent, resolve: { shows: LibraryResolverService } },
{ path: "show/:show-slug", component: ShowDetailsComponent, resolve: { show: ShowResolverService } },
{ path: "watch/:item", component: PlayerComponent, resolve: { show: StreamResolverService } },
{ path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService }, runGuardsAndResolvers: "always" },
{ path: "**", component: NotFoundComponent }
];

View File

@ -1 +1,58 @@
<p>player works!</p>
<div id="root">
<div class="player">
<video id="player" autoplay muted (click)="tooglePlayback()">
<source src="/api/video/{{this.video}}" type="video/mp4" />
</video>
</div>
<div class="back">
<button mat-icon-button data-toggle="tooltip" data-placement="bottom" title="Back" (click)="back()">
<mat-icon>arrow_back</mat-icon>
</button>
<h5>{{this.item.showTitle}}</h5>
</div>
<div class="controller container-fluid">
<div class="img">
<img src="thumb/{{this.item.showSlug}}" />
</div>
<div class="content">
<h3>S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}</h3>
<mat-progress-bar color="accent"></mat-progress-bar>
<div class="buttons">
<div class="left">
<button *ngIf="this.item.episodeNumber != 1" mat-icon-button data-toggle="tooltip" data-placement="top" title="Previous" routerLink="/watch/{{this.item.showSlug}}-s{{this.item.seasonNumber}}e{{this.item.episodeNumber - 1}}">
<mat-icon>skip_previous</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Play" id="play" (click)="tooglePlayback()">
<mat-icon>{{this.playIcon}}</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Next" routerLink="/watch/{{this.item.showSlug}}-s{{this.item.seasonNumber}}e{{this.item.episodeNumber + 1}}">
<mat-icon>skip_next</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Volume">
<mat-icon>volume_up</mat-icon>
</button>
<p>00:00 / --:--</p>
</div>
<div class="right">
<button *ngIf="this.item.audios != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select audio track">
<mat-icon>music_note</mat-icon>
</button>
<button *ngIf="this.item.subtitles != null" mat-icon-button data-toggle="tooltip" data-placement="top" title="Select subtitle track">
<mat-icon>closed_caption</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Cast">
<mat-icon>cast</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Settings">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Fullscreen" id="fullscreen" (click)="fullscreen()">
<mat-icon>{{fullscreenIcon}}</mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,116 @@
.player
{
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #000;
> video
{
width: 100%;
height: 100%;
object-fit: contain;
}
}
.back
{
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
padding: .33%;
display: flex;
> button
{
outline: none;
}
> h5
{
margin: 0;
margin-left: .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;
> mat-progress-bar
{
width: 100%;
height: 2px;
margin-top: 1rem;
margin-bottom: 1rem;
}
.buttons
{
display: flex;
flex-direction: row;
justify-content: space-between;
> div
{
&.left
{
align-self: start;
display: flex;
> p
{
margin: 0;
margin-left: 1rem;
align-self: center;
}
}
&.right
{
align-self: end;
}
> button
{
margin-left: .3rem;
margin-right: .3rem;
outline: none;
}
}
}
}
}

View File

@ -1,21 +1,93 @@
import { Component, OnInit } from '@angular/core';
import { WatchItem } from "../../models/watch-item";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
import { Location } from "@angular/common";
@Component({
selector: 'app-player',
templateUrl: './player.component.html',
styleUrls: ['./player.component.scss']
})
export class PlayerComponent implements OnInit {
export class PlayerComponent implements OnInit
{
item: WatchItem;
video: string;
constructor() { }
playIcon: string = "pause"; //Icon used by the play btn.
fullscreenIcon: string = "fullscreen"; //Icon used by the fullscreen btn.
private player: HTMLVideoElement;
constructor(private route: ActivatedRoute, private location: Location)
{
this.video = this.route.snapshot.paramMap.get("item");
}
ngOnInit()
{
document.getElementById("nav").classList.add("d-none");
this.item = this.route.snapshot.data.item;
console.log("Init");
}
ngAfterViewInit()
{
this.player = document.getElementById("player") as HTMLVideoElement;
this.player.controls = false;
$('[data-toggle="tooltip"]').tooltip();
document.addEventListener("fullscreenchange", (event) =>
{
if (document.fullscreenElement != null)
{
this.fullscreenIcon = "fullscreen_exit";
$("#fullscreen").attr("data-original-title", "Exit fullscreen").tooltip("show");
}
else
{
this.fullscreenIcon = "fullscreen";
$("#fullscreen").attr("data-original-title", "Fullscreen").tooltip("show");
}
});
}
ngOnDestroy()
{
document.getElementById("nav").classList.remove("d-none");
}
back()
{
this.location.back();
}
tooglePlayback()
{
let playBtn: HTMLElement = document.getElementById("play");
if (this.player.paused)
{
this.player.play();
this.playIcon = "pause"
$(playBtn).attr("data-original-title", "Pause").tooltip("show");
}
else
{
this.player.pause();
this.playIcon = "play_arrow"
$(playBtn).attr("data-original-title", "Play").tooltip("show");
}
}
fullscreen()
{
if (document.fullscreenElement == null)
document.getElementById("root").requestFullscreen();
else
document.exitFullscreen();
}
}

View File

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=watch-item.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"watch-item.js","sourceRoot":"","sources":["watch-item.ts"],"names":[],"mappings":""}

View File

@ -0,0 +1,20 @@
export interface WatchItem
{
showTitle: string;
showSlug: string;
seasonNumber: number;
episodeNumber: number;
title: string;
releaseDate;
audio: Stream[];
subtitles: Stream[];
}
export interface Stream
{
title: string;
language: string;
isDefault: boolean;
isForced: boolean;
format: string;
}

View File

@ -305,7 +305,7 @@ namespace Kyoo.InternalAPI
public WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber)
{
string query = "SELECT episodes.id, shows.title as showTitle, seasonNumber, episodeNumber, episodes.title, releaseDate, episodes.path FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber AND episodes.episodeNumber = $episodeNumber;";
string query = "SELECT episodes.id, shows.title as showTitle, shows.slug as showSlug, seasonNumber, episodeNumber, episodes.title, releaseDate, episodes.path FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber AND episodes.episodeNumber = $episodeNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{

View File

@ -31,6 +31,7 @@
<ItemGroup>
<None Remove="ClientApp\src\models\show.ts" />
<None Remove="ClientApp\src\models\watch-item.ts" />
</ItemGroup>
<ItemGroup>
@ -69,6 +70,7 @@
<ItemGroup>
<TypeScriptCompile Include="ClientApp\src\models\show.ts" />
<TypeScriptCompile Include="ClientApp\src\models\watch-item.ts" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">

View File

@ -11,6 +11,7 @@ namespace Kyoo.Models
[JsonIgnore] public readonly long episodeID;
public string ShowTitle;
public string ShowSlug;
public long seasonNumber;
public long episodeNumber;
public string Title;
@ -23,10 +24,11 @@ namespace Kyoo.Models
public WatchItem() { }
public WatchItem(long episodeID, string showTitle, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path)
public WatchItem(long episodeID, string showTitle, string showSlug, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path)
{
this.episodeID = episodeID;
ShowTitle = showTitle;
ShowSlug = showSlug;
this.seasonNumber = seasonNumber;
this.episodeNumber = episodeNumber;
Title = title;
@ -34,7 +36,7 @@ namespace Kyoo.Models
Path = path;
}
public WatchItem(long episodeID, string showTitle, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path, Stream[] audios, Stream[] subtitles) : this(episodeID, showTitle, seasonNumber, episodeNumber, title, releaseDate, path)
public WatchItem(long episodeID, string showTitle, string showSlug, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path, Stream[] audios, Stream[] subtitles) : this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path)
{
this.audios = audios;
this.subtitles = subtitles;
@ -44,6 +46,7 @@ namespace Kyoo.Models
{
return new WatchItem((long)reader["id"],
reader["showTitle"] as string,
reader["showSlug"] as string,
(long)reader["seasonNumber"],
(long)reader["episodeNumber"],
reader["title"] as string,