diff --git a/Kyoo/ClientApp/package-lock.json b/Kyoo/ClientApp/package-lock.json index 4d474975..606ee9b9 100644 --- a/Kyoo/ClientApp/package-lock.json +++ b/Kyoo/ClientApp/package-lock.json @@ -4358,6 +4358,11 @@ "integrity": "sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==", "dev": true }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=" + }, "handle-thing": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", diff --git a/Kyoo/ClientApp/package.json b/Kyoo/ClientApp/package.json index 3cd80f47..1da988cb 100644 --- a/Kyoo/ClientApp/package.json +++ b/Kyoo/ClientApp/package.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "~8.2.0", "@angular/router": "~8.2.0", "bootstrap": "^4.3.1", + "hammerjs": "^2.0.8", "jquery": "^3.4.1", "popper.js": "^1.15.0", "rxjs": "~6.4.0", diff --git a/Kyoo/ClientApp/src/app/app-routing.module.ts b/Kyoo/ClientApp/src/app/app-routing.module.ts index 723a2f82..9ab10171 100644 --- a/Kyoo/ClientApp/src/app/app-routing.module.ts +++ b/Kyoo/ClientApp/src/app/app-routing.module.ts @@ -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: { item: StreamResolverService }, runGuardsAndResolvers: "always" }, + { path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService } }, { path: "**", component: NotFoundComponent } ]; diff --git a/Kyoo/ClientApp/src/app/app.module.ts b/Kyoo/ClientApp/src/app/app.module.ts index e73cf94a..5914ed06 100644 --- a/Kyoo/ClientApp/src/app/app.module.ts +++ b/Kyoo/ClientApp/src/app/app.module.ts @@ -5,6 +5,7 @@ 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 { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -36,7 +37,8 @@ import { ShowDetailsComponent } from './show-details/show-details.component'; MatButtonModule, MatIconModule, MatSelectModule, - MatMenuModule + MatMenuModule, + MatSliderModule ], providers: [], bootstrap: [AppComponent] diff --git a/Kyoo/ClientApp/src/app/episodes-list/episodes-list.component.ts b/Kyoo/ClientApp/src/app/episodes-list/episodes-list.component.ts index 8c9710ee..56638727 100644 --- a/Kyoo/ClientApp/src/app/episodes-list/episodes-list.component.ts +++ b/Kyoo/ClientApp/src/app/episodes-list/episodes-list.component.ts @@ -56,9 +56,4 @@ export class EpisodesListComponent implements OnInit return offset; } - - play(episode: Episode) - { - - } } diff --git a/Kyoo/ClientApp/src/app/player/player.component.html b/Kyoo/ClientApp/src/app/player/player.component.html index 36e26e12..3f611c70 100644 --- a/Kyoo/ClientApp/src/app/player/player.component.html +++ b/Kyoo/ClientApp/src/app/player/player.component.html @@ -1,56 +1,70 @@
-
-
- -
{{this.item.showTitle}}
-
- -
-
- +
+
+ +
{{this.item.showTitle}}
-
-

S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}

- -
-
- - - - -

00:00 / --:--

-
-
- - - - - + +
+
+ +
+
+

S{{this.item.seasonNumber}}:E{{this.item.episodeNumber}} - {{this.item.title}}

+ +
+
+ + + + +

{{this.minutes | number: '2.0-0'}}:{{this.seconds | number: '2.0-0'}} / --:--

+
+
+ + + + + +
diff --git a/Kyoo/ClientApp/src/app/player/player.component.scss b/Kyoo/ClientApp/src/app/player/player.component.scss index 796ca57d..c9d2926d 100644 --- a/Kyoo/ClientApp/src/app/player/player.component.scss +++ b/Kyoo/ClientApp/src/app/player/player.component.scss @@ -114,3 +114,62 @@ } } } + + +#nextBtn +{ + position: relative; + + &: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; + } + } + } +} diff --git a/Kyoo/ClientApp/src/app/player/player.component.ts b/Kyoo/ClientApp/src/app/player/player.component.ts index fa6a1871..0d170398 100644 --- a/Kyoo/ClientApp/src/app/player/player.component.ts +++ b/Kyoo/ClientApp/src/app/player/player.component.ts @@ -12,22 +12,31 @@ import { Location } from "@angular/common"; export class PlayerComponent implements OnInit { item: WatchItem; - video: string; + + hours: number; + minutes: number; + seconds: number; 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"); - } + constructor(private route: ActivatedRoute, private sanitizer: DomSanitizer, private location: Location) { } ngOnInit() { document.getElementById("nav").classList.add("d-none"); - this.item = this.route.snapshot.data.item; + this.route.data.subscribe((data) => + { + this.item = data.item; + + if (this.player) + { + this.player.load(); + this.initPlayBtn(); + } + }); console.log("Init"); } @@ -36,9 +45,25 @@ export class PlayerComponent implements OnInit this.player = document.getElementById("player") as HTMLVideoElement; this.player.controls = false; - $('[data-toggle="tooltip"]').tooltip(); + this.player.onplay = () => + { + this.initPlayBtn(); + } - document.addEventListener("fullscreenchange", (event) => + this.player.onpause = () => + { + this.playIcon = "play_arrow" + $("#play").attr("data-original-title", "Play").tooltip("show"); + } + + this.player.ontimeupdate = () => + { + this.seconds = Math.round(this.player.currentTime % 60); + this.minutes = Math.round(this.player.currentTime / 60); + }; + + + document.addEventListener("fullscreenchange", () => { if (document.fullscreenElement != null) { @@ -51,6 +76,8 @@ export class PlayerComponent implements OnInit $("#fullscreen").attr("data-original-title", "Fullscreen").tooltip("show"); } }); + + $('[data-toggle="tooltip"]').tooltip(); } ngOnDestroy() @@ -65,22 +92,16 @@ export class PlayerComponent implements OnInit 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"); - } + initPlayBtn() + { + this.playIcon = "pause"; + $("#play").attr("data-original-title", "Pause").tooltip("show"); } fullscreen() @@ -90,4 +111,10 @@ export class PlayerComponent implements OnInit else document.exitFullscreen(); } + + + getThumb(url: string) + { + return this.sanitizer.bypassSecurityTrustStyle("url(" + url + ")"); + } } diff --git a/Kyoo/ClientApp/src/main.ts b/Kyoo/ClientApp/src/main.ts index c7b673cf..c8d4c2ad 100644 --- a/Kyoo/ClientApp/src/main.ts +++ b/Kyoo/ClientApp/src/main.ts @@ -3,6 +3,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; +import "hammerjs" if (environment.production) { enableProdMode(); diff --git a/Kyoo/ClientApp/src/models/episode.js b/Kyoo/ClientApp/src/models/episode.js new file mode 100644 index 00000000..b651890b --- /dev/null +++ b/Kyoo/ClientApp/src/models/episode.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=episode.js.map \ No newline at end of file diff --git a/Kyoo/ClientApp/src/models/episode.js.map b/Kyoo/ClientApp/src/models/episode.js.map new file mode 100644 index 00000000..edbd2c6e --- /dev/null +++ b/Kyoo/ClientApp/src/models/episode.js.map @@ -0,0 +1 @@ +{"version":3,"file":"episode.js","sourceRoot":"","sources":["episode.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/Kyoo/ClientApp/src/models/episode.ts b/Kyoo/ClientApp/src/models/episode.ts index fbb7c7c1..64093ab3 100644 --- a/Kyoo/ClientApp/src/models/episode.ts +++ b/Kyoo/ClientApp/src/models/episode.ts @@ -2,6 +2,8 @@ export interface Episode { episodeNumber: number; title: string; + thumb: string; + link: string; overview: string; releaseDate; runtimeInMinutes: number; diff --git a/Kyoo/ClientApp/src/models/watch-item.ts b/Kyoo/ClientApp/src/models/watch-item.ts index eaa07a55..266280f7 100644 --- a/Kyoo/ClientApp/src/models/watch-item.ts +++ b/Kyoo/ClientApp/src/models/watch-item.ts @@ -1,11 +1,19 @@ +import { Episode } from "./episode"; + export interface WatchItem { showTitle: string; showSlug: string; seasonNumber: number; episodeNumber: number; + video: string; title: string; + link: string; releaseDate; + + previousEpisode: string; + nextEpisode: Episode; + audio: Stream[]; subtitles: Stream[]; } diff --git a/Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs b/Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs index a1cefe6b..79c9ac2e 100644 --- a/Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs +++ b/Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs @@ -14,6 +14,7 @@ namespace Kyoo.InternalAPI List GetPeople(long showID); List GetGenreForShow(long showID); List GetSeasons(long showID); + int GetSeasonCount(string showSlug, long seasonNumber); (VideoStream video, List audios, List subtitles) GetStreams(long episodeID); //Public read @@ -22,7 +23,7 @@ namespace Kyoo.InternalAPI Season GetSeason(string showSlug, long seasonNumber); List GetEpisodes(string showSlug, long seasonNumber); Episode GetEpisode(string showSlug, long seasonNumber, long episodeNumber); - WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber); + WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber, bool complete = true); People GetPeopleBySlug(string slug); Genre GetGenreBySlug(string slug); Studio GetStudioBySlug(string slug); diff --git a/Kyoo/InternalAPI/LibraryManager/LibraryManager.cs b/Kyoo/InternalAPI/LibraryManager/LibraryManager.cs index be3958fd..55870ab8 100644 --- a/Kyoo/InternalAPI/LibraryManager/LibraryManager.cs +++ b/Kyoo/InternalAPI/LibraryManager/LibraryManager.cs @@ -1,6 +1,7 @@ using Kyoo.Models; using Kyoo.Models.Watch; using Microsoft.Extensions.Configuration; +using System; using System.Collections.Generic; using System.Data.SQLite; using System.Diagnostics; @@ -233,7 +234,7 @@ namespace Kyoo.InternalAPI public List GetSeasons(long showID) { - string query = "SELECT * FROM seasons WHERE showID = $showID;"; + string query = "SELECT * FROM seasons WHERE showID = $showID ORDER BY seasonNumber;"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { @@ -266,9 +267,23 @@ namespace Kyoo.InternalAPI } } + public int GetSeasonCount(string showSlug, long seasonNumber) + { + string query = "SELECT count(episodes.id) FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber;"; + + using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) + { + cmd.Parameters.AddWithValue("$showSlug", showSlug); + cmd.Parameters.AddWithValue("$seasonNumber", seasonNumber); + + int count = Convert.ToInt32(cmd.ExecuteScalar()); + return count; + } + } + public List GetEpisodes(string showSlug, long seasonNumber) { - string query = "SELECT * FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber;"; + string query = "SELECT * FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber ORDER BY episodeNumber;"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { @@ -303,7 +318,7 @@ namespace Kyoo.InternalAPI } } - public WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber) + public WatchItem GetWatchItem(string showSlug, long seasonNumber, long episodeNumber, bool complete = true) { 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;"; @@ -315,7 +330,12 @@ namespace Kyoo.InternalAPI SQLiteDataReader reader = cmd.ExecuteReader(); if (reader.Read()) - return WatchItem.FromReader(reader).SetStreams(this); + { + if (complete) + return WatchItem.FromReader(reader).SetStreams(this).SetPrevious(this).SetNext(this); + else + return WatchItem.FromReader(reader); + } else return null; } diff --git a/Kyoo/Models/Episode.cs b/Kyoo/Models/Episode.cs index 4b9dd3ad..dcd47b61 100644 --- a/Kyoo/Models/Episode.cs +++ b/Kyoo/Models/Episode.cs @@ -21,6 +21,7 @@ namespace Kyoo.Models [JsonIgnore] public string ImgPrimary; public string ExternalIDs; + public string Link; //Used only on the player public string Thumb; //Used in the API response only @@ -77,6 +78,7 @@ namespace Kyoo.Models public Episode SetThumb(string showSlug) { Thumb = "thumb/" + showSlug + "/s" + seasonNumber + "/e" + episodeNumber; + Link = showSlug + "-s" + seasonNumber + "e" + episodeNumber; return this; } } diff --git a/Kyoo/Models/WatchItem.cs b/Kyoo/Models/WatchItem.cs index 52e1dc48..d7fcfe3b 100644 --- a/Kyoo/Models/WatchItem.cs +++ b/Kyoo/Models/WatchItem.cs @@ -15,8 +15,11 @@ namespace Kyoo.Models public long seasonNumber; public long episodeNumber; public string Title; + public string Link; public DateTime? ReleaseDate; [JsonIgnore] public string Path; + public string previousEpisode; + public Episode nextEpisode; [JsonIgnore] public VideoStream video; public IEnumerable audios; @@ -34,6 +37,8 @@ namespace Kyoo.Models Title = title; ReleaseDate = releaseDate; Path = path; + + Link = ShowSlug + "-s" + seasonNumber + "e" + episodeNumber; } 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) @@ -62,5 +67,29 @@ namespace Kyoo.Models subtitles = streams.subtitles; return this; } + + public WatchItem SetPrevious(ILibraryManager libraryManager) + { + long lastEp = episodeNumber - 1; + if(lastEp > 0) + previousEpisode = ShowSlug + "-s" + seasonNumber + "e" + lastEp; + else if(seasonNumber > 1) + { + int seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber - 1); + previousEpisode = ShowSlug + "-s" + (seasonNumber - 1) + "e" + seasonCount; + } + return this; + } + + public WatchItem SetNext(ILibraryManager libraryManager) + { + long seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber); + if (episodeNumber >= seasonCount) + nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber + 1, 1); + else + nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber, episodeNumber + 1); + + return this; + } } }