[DEV] add prototype of store of the playing position

This commit is contained in:
Edouard DUPIN 2023-08-13 19:32:24 +02:00
parent 9892e10312
commit 82cba33e99
21 changed files with 22396 additions and 22017 deletions

View File

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>kar</groupId> <groupId>kar</groupId>
<artifactId>karideo</artifactId> <artifactId>karideo</artifactId>
<version>0.1.0</version> <version>0.2.0</version>
<properties> <properties>
<maven.compiler.version>3.1</maven.compiler.version> <maven.compiler.version>3.1</maven.compiler.version>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
@ -20,7 +20,7 @@
<dependency> <dependency>
<groupId>kangaroo-and-rabbit</groupId> <groupId>kangaroo-and-rabbit</groupId>
<artifactId>archidata</artifactId> <artifactId>archidata</artifactId>
<version>0.3.7</version> <version>0.3.8</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -8,6 +8,7 @@ import org.glassfish.jersey.server.ResourceConfig;
import org.kar.karideo.api.*; import org.kar.karideo.api.*;
import org.kar.karideo.filter.KarideoAuthenticationFilter; import org.kar.karideo.filter.KarideoAuthenticationFilter;
import org.kar.karideo.migration.Initialization; import org.kar.karideo.migration.Initialization;
import org.kar.karideo.migration.Migration20230810;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.kar.archidata.GlobalConfiguration; import org.kar.archidata.GlobalConfiguration;
@ -45,7 +46,7 @@ public class WebLauncher {
WebLauncher.LOGGER.info("Add initialization"); WebLauncher.LOGGER.info("Add initialization");
migrationEngine.setInit(new Initialization()); migrationEngine.setInit(new Initialization());
WebLauncher.LOGGER.info("Add migration since last version"); WebLauncher.LOGGER.info("Add migration since last version");
// NOTHING for now migrationEngine.add(new Migration20230810());
WebLauncher.LOGGER.info("Migrate the DB [START]"); WebLauncher.LOGGER.info("Migrate the DB [START]");
migrationEngine.migrate(GlobalConfiguration.dbConfig); migrationEngine.migrate(GlobalConfiguration.dbConfig);
WebLauncher.LOGGER.info("Migrate the DB [STOP]"); WebLauncher.LOGGER.info("Migrate the DB [STOP]");
@ -91,6 +92,7 @@ public class WebLauncher {
rc.register(SeasonResource.class); rc.register(SeasonResource.class);
rc.register(TypeResource.class); rc.register(TypeResource.class);
rc.register(VideoResource.class); rc.register(VideoResource.class);
rc.register(UserMediaAdvancementResource.class);
rc.register(HealthCheck.class); rc.register(HealthCheck.class);
rc.register(Front.class); rc.register(Front.class);

View File

@ -1,15 +1,19 @@
package org.kar.karideo.api; package org.kar.karideo.api;
import org.kar.archidata.annotation.security.PermitAll; import org.kar.archidata.annotation.security.PermitAll;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.kar.archidata.util.JWTWrapper; import org.kar.archidata.util.JWTWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/health_check") @Path("/health_check")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public class HealthCheck { public class HealthCheck {
static final Logger LOGGER = LoggerFactory.getLogger(HealthCheck.class);
public class HealthResult { public class HealthResult {
public String value; public String value;
public HealthResult(String value) { public HealthResult(String value) {

View File

@ -3,6 +3,8 @@ package org.kar.karideo.api;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam; import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.karideo.model.Season; import org.kar.karideo.model.Season;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kar.archidata.SqlWrapper; import org.kar.archidata.SqlWrapper;
import org.kar.archidata.annotation.security.RolesAllowed; import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.util.DataTools; import org.kar.archidata.util.DataTools;
@ -16,6 +18,7 @@ import java.util.List;
@Path("/season") @Path("/season")
@Produces({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON})
public class SeasonResource { public class SeasonResource {
static final Logger LOGGER = LoggerFactory.getLogger(SeasonResource.class);
@GET @GET
@Path("{id}") @Path("{id}")

View File

@ -4,6 +4,8 @@ import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam; import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.karideo.model.Season; import org.kar.karideo.model.Season;
import org.kar.karideo.model.Series; import org.kar.karideo.model.Series;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kar.archidata.SqlWrapper; import org.kar.archidata.SqlWrapper;
import org.kar.archidata.annotation.security.RolesAllowed; import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.util.DataTools; import org.kar.archidata.util.DataTools;
@ -17,6 +19,7 @@ import java.util.List;
@Path("/series") @Path("/series")
@Produces({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON})
public class SeriesResource { public class SeriesResource {
static final Logger LOGGER = LoggerFactory.getLogger(SeriesResource.class);
@GET @GET
@Path("{id}") @Path("{id}")

View File

@ -3,6 +3,8 @@ package org.kar.karideo.api;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam; import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.karideo.model.Type; import org.kar.karideo.model.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kar.archidata.SqlWrapper; import org.kar.archidata.SqlWrapper;
import org.kar.archidata.annotation.security.RolesAllowed; import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.util.DataTools; import org.kar.archidata.util.DataTools;
@ -16,6 +18,7 @@ import java.util.List;
@Path("/type") @Path("/type")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public class TypeResource { public class TypeResource {
static final Logger LOGGER = LoggerFactory.getLogger(TypeResource.class);
@GET @GET

View File

@ -0,0 +1,103 @@
package org.kar.karideo.api;
import org.kar.karideo.model.UserMediaAdvancement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kar.archidata.SqlWrapper;
import org.kar.archidata.WhereCondition;
import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.filter.GenericContext;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import java.util.List;
@Path("/advancement")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public class UserMediaAdvancementResource {
static final Logger LOGGER = LoggerFactory.getLogger(UserMediaAdvancementResource.class);
@GET
@Path("{id}")
@RolesAllowed("USER")
public UserMediaAdvancement getWithId(@Context SecurityContext sc, @PathParam("id") Long id) throws Exception {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
return SqlWrapper.getWhere(UserMediaAdvancement.class,
List.of(
new WhereCondition("mediaId", "=", id),
new WhereCondition("userId", "=", gc.userByToken.id),
new WhereCondition("deleted", "=", 0)
), false);
}
@GET
@RolesAllowed("USER")
public List<UserMediaAdvancement> gets(@Context SecurityContext sc ) throws Exception {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
return SqlWrapper.getsWhere(UserMediaAdvancement.class,
List.of(
new WhereCondition("userId", "=", gc.userByToken.id),
new WhereCondition("deleted", "=", 0)
), false);
}
/* =============================================================================
* Modification SECTION:
* ============================================================================= */
public record MediaInformations(int time, float percent, int count) {};
//@POST
//@Path("{id}")
//@RolesAllowed("USER")
//@Consumes(MediaType.APPLICATION_JSON)
public UserMediaAdvancement post(@Context SecurityContext sc, @PathParam("id") Long id, MediaInformations data) throws Exception {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
UserMediaAdvancement elem = new UserMediaAdvancement();
elem.userId = gc.userByToken.id;
elem.mediaId = id;
elem.time = data.time;
elem.percent = data.percent;
elem.count = data.count;
return SqlWrapper.insert(elem);
}
public record MediaInformationsDelta(int time, float percent, boolean addCount) {};
@PUT
@Path("{id}")
@RolesAllowed("USER")
@Consumes(MediaType.APPLICATION_JSON)
public UserMediaAdvancement put(@Context SecurityContext sc, @PathParam("id") Long id, MediaInformationsDelta data) throws Exception {
UserMediaAdvancement elem = this.getWithId(sc, id);
if (elem == null) {
// insert element
return this.post(sc, id, new MediaInformations(data.time(), data.percent(), 0));
}
elem.time = data.time;
elem.percent = data.percent;
if (data.addCount) {
elem.count++;
}
LOGGER.info("{},{},{}", elem.time, elem.percent, elem.count);
int nbAfected = SqlWrapper.update(elem, elem.id, List.of("time", "percent","count"));
// TODO: manage the fact that no element has been updated ...
UserMediaAdvancement ret = SqlWrapper.get(UserMediaAdvancement.class, elem.id);
return ret;
}
@DELETE
@Path("{id}")
@RolesAllowed("USER")
public Response delete(@Context SecurityContext sc, @PathParam("id") Long id) throws Exception {
UserMediaAdvancement elem = this.getWithId(sc, id);
SqlWrapper.setDelete(UserMediaAdvancement.class, elem.id);
return Response.ok().build();
}
}

View File

@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
@Path("/users") @Path("/users")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public class UserResource { public class UserResource {
final Logger logger = LoggerFactory.getLogger(UserResource.class); static final Logger LOGGER = LoggerFactory.getLogger(UserResource.class);
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class UserOut { public class UserOut {
@ -73,9 +73,9 @@ public class UserResource {
@Path("me") @Path("me")
@RolesAllowed("USER") @RolesAllowed("USER")
public UserOut getMe(@Context SecurityContext sc) { public UserOut getMe(@Context SecurityContext sc) {
logger.debug("getMe()"); LOGGER.debug("getMe()");
GenericContext gc = (GenericContext) sc.getUserPrincipal(); GenericContext gc = (GenericContext) sc.getUserPrincipal();
logger.debug("== USER ? {}", gc.userByToken); LOGGER.debug("== USER ? {}", gc.userByToken);
return new UserOut(gc.userByToken.id, gc.userByToken.name); return new UserOut(gc.userByToken.id, gc.userByToken.name);
} }

View File

@ -8,6 +8,8 @@ import org.kar.karideo.model.Media;
import org.kar.karideo.model.Season; import org.kar.karideo.model.Season;
import org.kar.karideo.model.Series; import org.kar.karideo.model.Series;
import org.kar.karideo.model.Type; import org.kar.karideo.model.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kar.archidata.SqlWrapper; import org.kar.archidata.SqlWrapper;
import org.kar.archidata.annotation.security.RolesAllowed; import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.api.DataResource; import org.kar.archidata.api.DataResource;
@ -26,6 +28,7 @@ import java.util.List;
@Path("/video") @Path("/video")
@Produces({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON})
public class VideoResource { public class VideoResource {
static final Logger LOGGER = LoggerFactory.getLogger(VideoResource.class);
@GET @GET
@RolesAllowed("USER") @RolesAllowed("USER")

View File

@ -7,6 +7,7 @@ import org.kar.karideo.model.Media;
import org.kar.karideo.model.Season; import org.kar.karideo.model.Season;
import org.kar.karideo.model.Series; import org.kar.karideo.model.Series;
import org.kar.karideo.model.Type; import org.kar.karideo.model.Type;
import org.kar.karideo.model.UserMediaAdvancement;
public class Initialization extends MigrationSqlStep { public class Initialization extends MigrationSqlStep {
@ -24,6 +25,7 @@ public class Initialization extends MigrationSqlStep {
addClass(Series.class); addClass(Series.class);
addClass(Season.class); addClass(Season.class);
addClass(User.class); addClass(User.class);
addClass(UserMediaAdvancement.class);
addAction(""" addAction("""
INSERT INTO `type` (`id`, `name`, `description`) VALUES INSERT INTO `type` (`id`, `name`, `description`) VALUES
@ -54,6 +56,9 @@ public class Initialization extends MigrationSqlStep {
addAction(""" addAction("""
ALTER TABLE `season` AUTO_INCREMENT = 1000; ALTER TABLE `season` AUTO_INCREMENT = 1000;
"""); """);
addAction("""
ALTER TABLE `userMediaAdvencement` AUTO_INCREMENT = 1000;
""");
} }
} }

View File

@ -0,0 +1,23 @@
package org.kar.karideo.migration;
import org.kar.archidata.migration.MigrationSqlStep;
import org.kar.karideo.model.UserMediaAdvancement;
public class Migration20230810 extends MigrationSqlStep {
public static final int KARSO_INITIALISATION_ID = 1;
@Override
public String getName() {
return "migration-2023-08-10";
}
public Migration20230810() throws Exception {
addClass(UserMediaAdvancement.class);
addAction("""
ALTER TABLE `userMediaAdvencement` AUTO_INCREMENT = 1000;
""");
}
}

View File

@ -0,0 +1,34 @@
package org.kar.karideo.model;
import org.kar.archidata.annotation.SQLComment;
import org.kar.archidata.annotation.SQLForeignKey;
import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLNotNull;
import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.model.GenericTable;
import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("userMediaAdvancement")
@SQLIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserMediaAdvancement extends GenericTable {
@SQLNotNull
@SQLComment("Foreign Key Id of the user")
@SQLForeignKey("user")
public long userId;
@SQLNotNull
@SQLComment("Id of the media")
@SQLForeignKey("media")
public long mediaId;
@SQLNotNull
@SQLComment("Percent of admencement in the media")
public float percent;
@SQLNotNull
@SQLComment("Number of second of admencement in the media")
public int time;
@SQLNotNull
@SQLComment("Number of time this media has been read")
public int count;
}

View File

@ -7,7 +7,7 @@
"": { "": {
"name": "karideo", "name": "karideo",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MPL-2",
"dependencies": { "dependencies": {
"@angular/animations": "^14.2.10", "@angular/animations": "^14.2.10",
"@angular/cdk": "^14.2.7", "@angular/cdk": "^14.2.7",

View File

@ -8,31 +8,23 @@ import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; // this is needed for dynamic selection of the select
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { UploadFileComponent } from 'common/component/upload-file/upload-file';
import { ElementDataImageComponent } from './component/data-image/data-image';
import { ElementTypeComponent } from './component/element-type/element-type'; import { ElementTypeComponent } from './component/element-type/element-type';
import { ElementSeriesComponent } from './component/element-series/element-series'; import { ElementSeriesComponent } from './component/element-series/element-series';
import { ElementSeasonComponent } from './component/element-season/element-season'; import { ElementSeasonComponent } from './component/element-season/element-season';
import { ElementVideoComponent } from './component/element-video/element-video'; import { ElementVideoComponent } from './component/element-video/element-video';
import { PopInComponent } from 'common/component/popin/popin';
import { PopInCreateType } from './popin/create-type/create-type'; import { PopInCreateType } from './popin/create-type/create-type';
import { PopInUploadProgress } from 'common/popin/upload-progress/upload-progress';
import { PopInDeleteConfirm } from 'common/popin/delete-confirm/delete-confirm';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { import {
HomeScene, HelpScene, TypeScene, SeriesScene, SeasonScene, VideoScene, SettingsScene, HomeScene, HelpScene, TypeScene, SeriesScene, SeasonScene, VideoScene, SettingsScene,
VideoEditScene, SeasonEditScene, SeriesEditScene VideoEditScene, SeasonEditScene, SeriesEditScene
} from './scene'; } from './scene';
import { TypeService, DataService, SeriesService, SeasonService, VideoService, ArianeService } from './service'; import { TypeService, DataService, SeriesService, SeasonService, VideoService, ArianeService, AdvancementService } from './service';
import { BddService, CookiesService, HttpWrapperService, OnlyAdminGuard, OnlyUnregisteredGuardHome, OnlyUsersGuard, OnlyUsersGuardHome, PopInService, SessionService, SSOService, StorageService, UserService } from 'common/service';
import { ErrorViewerScene, ForbiddenScene, HomeOutScene, NotFound404Scene, SsoScene } from 'common/scene';
import { UploadScene } from './scene/upload/upload'; import { UploadScene } from './scene/upload/upload';
import { AsyncActionStatusComponent, BurgerPropertyComponent, CheckboxComponent, EntryComponent, EntryNumberComponent, EntryValidatorComponent, ErrorComponent, ErrorMessageStateComponent, PasswordEntryComponent, RenderFormComponent, RenderSettingsComponent, SpinerComponent, TopMenuComponent } from 'common/component'; import { common_module_declarations, common_module_imports, common_module_providers, common_module_exports } from 'common/module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -42,30 +34,10 @@ import { AsyncActionStatusComponent, BurgerPropertyComponent, CheckboxComponent,
ElementVideoComponent, ElementVideoComponent,
AppComponent, AppComponent,
TopMenuComponent,
UploadFileComponent,
ErrorComponent,
PasswordEntryComponent,
EntryComponent,
EntryValidatorComponent,
SpinerComponent,
AsyncActionStatusComponent,
ErrorMessageStateComponent,
CheckboxComponent,
BurgerPropertyComponent,
RenderSettingsComponent,
RenderFormComponent,
EntryNumberComponent,
PopInComponent,
PopInCreateType, PopInCreateType,
PopInUploadProgress,
PopInDeleteConfirm,
HomeScene, HomeScene,
ErrorViewerScene,
HelpScene, HelpScene,
SsoScene,
TypeScene, TypeScene,
SeriesScene, SeriesScene,
SeasonScene, SeasonScene,
@ -75,52 +47,33 @@ import { AsyncActionStatusComponent, BurgerPropertyComponent, CheckboxComponent,
SeasonEditScene, SeasonEditScene,
SeriesEditScene, SeriesEditScene,
UploadScene, UploadScene,
ForbiddenScene, ...common_module_declarations,
HomeOutScene,
NotFound404Scene,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
RouterModule, RouterModule,
AppRoutingModule, AppRoutingModule,
HttpClientModule, HttpClientModule,
FormsModule, ...common_module_imports,
ReactiveFormsModule,
], ],
providers: [ providers: [
PopInService,
HttpWrapperService,
SessionService,
CookiesService,
StorageService,
UserService,
SSOService,
BddService,
TypeService, TypeService,
DataService, DataService,
SeriesService, SeriesService,
SeasonService, SeasonService,
VideoService, VideoService,
ArianeService, ArianeService,
OnlyUsersGuard, AdvancementService,
OnlyAdminGuard, ...common_module_providers,
OnlyUsersGuardHome,
OnlyUnregisteredGuardHome,
], ],
exports: [ exports: [
AppComponent, AppComponent,
TopMenuComponent,
UploadFileComponent,
ErrorComponent,
ElementTypeComponent, ElementTypeComponent,
ElementSeriesComponent, ElementSeriesComponent,
ElementSeasonComponent, ElementSeasonComponent,
ElementVideoComponent, ElementVideoComponent,
PopInCreateType, PopInCreateType,
...common_module_exports,
PopInComponent,
PopInUploadProgress,
PopInDeleteConfirm,
], ],
bootstrap: [ bootstrap: [
AppComponent AppComponent

View File

@ -0,0 +1,37 @@
import { isNumberFinite, isString, isOptionalOf, isNumber } from "common/utils";
import { isNodeData, NodeData } from "common/model/node";
export interface UserMediaAdvancement {
id: number;
// Id of the media
mediaId?: number;
// Percent of advancement in the media
percent?: number;
// "Number of second of advancement in the media
time?: number;
// Number of time this media has been read
count?: number;
};
export function isUserMediaAdvancement(data: any): data is UserMediaAdvancement {
if (!isNumberFinite(data.id)) {
return false;
}
if (!isNumberFinite(data.mediaId)) {
return false;
}
if (!isNumber(data.percent)) {
return false;
}
if (!isNumberFinite(data.time)) {
return false;
}
if (!isNumberFinite(data.count)) {
return false;
}
return true;
}

View File

@ -53,28 +53,23 @@
<div class="episode"> <div class="episode">
<b>generatedName:</b> {{generatedName}} <b>generatedName:</b> {{generatedName}}
</div> </div>
<div class="description">
count= {{userMetaData?.count}} view percent={{userMetaData?.percent}}
percent={{convertIndisplayTime(userMetaData?.time)}}
</div>
<div class="description"> <div class="description">
{{description}} {{description}}
</div> </div>
</div> </div>
<div class="fill-all bg-black" *ngIf="playVideo"> <div class="fill-all bg-black" *ngIf="playVideo">
<div class="video" <div class="video" #globalVideoElement (mousemove)="startHideTimer()"
#globalVideoElement
(mousemove)="startHideTimer()"
(fullscreenchange)="onFullscreenChange($event)"> (fullscreenchange)="onFullscreenChange($event)">
<div class="video-elem"> <div class="video-elem">
<video src="{{videoSource}}" <video src="{{videoSource}}" #videoPlayer preload (play)="changeStateToPlay()"
#videoPlayer (pause)="changeStateToPause()" (timeupdate)="changeTimeupdate($event.currentTime)"
preload (durationchange)="changeDurationchange($event.duration)" (loadedmetadata)="changeMetadata()"
(play)="changeStateToPlay()" (audioTracks)="audioTracks($event)" autoplay (ended)="onVideoEnded()"><!-- controls > -->
(pause)="changeStateToPause()" <!--preload="none"-->
(timeupdate)="changeTimeupdate($event.currentTime)"
(durationchange)="changeDurationchange($event.duration)"
(loadedmetadata)="changeMetadata()"
(audioTracks)="audioTracks($event)"
autoplay
(ended)="onVideoEnded()"
><!-- controls > --> <!--preload="none"-->
<!--<p>Your browser does not support HTML5 video player. download video: <a href="{{videoSource}}>link here</a>.</p>--> <!--<p>Your browser does not support HTML5 video player. download video: <a href="{{videoSource}}>link here</a>.</p>-->
</video> </video>
</div> </div>
@ -84,9 +79,7 @@
<button (click)="onStop()"><i class="material-icons">stop</i></button> <button (click)="onStop()"><i class="material-icons">stop</i></button>
<div class="timer"> <div class="timer">
<div> <div>
<input type="range" min="0" class="slider" <input type="range" min="0" class="slider" [value]="currentTime" [max]="duration"
[value]="currentTime"
[max]="duration"
(input)="seek($event.target)"> (input)="seek($event.target)">
</div> </div>
<div class="timer-text"> <div class="timer-text">
@ -99,13 +92,17 @@
<!--<button (click)="onNext()"><i class="material-icons">navigate_next</i></button>--> <!--<button (click)="onNext()"><i class="material-icons">navigate_next</i></button>-->
<!--<button (click)="onMore()" ><i class="material-icons">more_vert</i></button>--> <!--<button (click)="onMore()" ><i class="material-icons">more_vert</i></button>-->
<button (click)="onFullscreen()" *ngIf="!isFullScreen"><i class="material-icons">fullscreen</i></button> <button (click)="onFullscreen()" *ngIf="!isFullScreen"><i class="material-icons">fullscreen</i></button>
<button (click)="onFullscreenExit()" *ngIf="isFullScreen"><i class="material-icons">fullscreen_exit</i></button> <button (click)="onFullscreenExit()" *ngIf="isFullScreen"><i
class="material-icons">fullscreen_exit</i></button>
<!--<button (click)="onTakeScreenShoot()"><i class="material-icons">add_a_photo</i></button>--> <!--<button (click)="onTakeScreenShoot()"><i class="material-icons">add_a_photo</i></button>-->
<button (click)="onVolumeMenu()"><i class="material-icons">volume_up</i></button> <button (click)="onVolumeMenu()"><i class="material-icons">volume_up</i></button>
<button class="bigPause" (click)="onPauseToggle()"><i *ngIf="!isPlaying" class="material-icons">play_circle_outline</i></button> <button class="bigPause" (click)="onPauseToggle()"><i *ngIf="!isPlaying"
<button class="bigRewind" (click)="onRewind()"><i *ngIf="!isPlaying" class="material-icons">fast_rewind</i></button> class="material-icons">play_circle_outline</i></button>
<button class="bigForward" (click)="onForward()"><i *ngIf="!isPlaying" class="material-icons">fast_forward</i></button> <button class="bigRewind" (click)="onRewind()"><i *ngIf="!isPlaying"
class="material-icons">fast_rewind</i></button>
<button class="bigForward" (click)="onForward()"><i *ngIf="!isPlaying"
class="material-icons">fast_forward</i></button>
</div> </div>
<div class="title-inline" *ngIf="!isFullScreen || !isPlaying"> <div class="title-inline" *ngIf="!isFullScreen || !isPlaying">
@ -119,12 +116,13 @@
<div class="volume" *ngIf="displayVolumeMenu && (!displayNeedHide || !isPlaying)"> <div class="volume" *ngIf="displayVolumeMenu && (!displayNeedHide || !isPlaying)">
<div class="volume-menu"> <div class="volume-menu">
<div class="slidecontainer"> <div class="slidecontainer">
<input type="range" min="0" max="100" class="slider" <input type="range" min="0" max="100" class="slider" [value]="volumeValue"
[value]="volumeValue"
(input)="onVolume($event.target)"> (input)="onVolume($event.target)">
</div> </div>
<button (click)="onVolumeMute()" *ngIf="!videoPlayer.muted"><i class="material-icons">volume_mute</i></button> <button (click)="onVolumeMute()" *ngIf="!videoPlayer.muted"><i
<button (click)="onVolumeUnMute()" *ngIf="videoPlayer.muted"><i class="material-icons">volume_off</i></button> class="material-icons">volume_mute</i></button>
<button (click)="onVolumeUnMute()" *ngIf="videoPlayer.muted"><i
class="material-icons">volume_off</i></button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,8 +5,8 @@
*/ */
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { UserMediaAdvancement } from 'app/model/user-media-advancement';
import { DataService, VideoService, SeriesService, SeasonService, ArianeService } from 'app/service'; import { DataService, VideoService, SeriesService, SeasonService, ArianeService, AdvancementService } from 'app/service';
import { HttpWrapperService } from 'common/service'; import { HttpWrapperService } from 'common/service';
import { isNullOrUndefined } from 'common/utils'; import { isNullOrUndefined } from 'common/utils';
@ -20,21 +20,21 @@ export class VideoScene implements OnInit {
videoGlobal: any; videoGlobal: any;
@ViewChild('globalVideoElement') @ViewChild('globalVideoElement')
set mainDivEl(el: ElementRef) { set mainDivEl(el: ElementRef) {
if(el !== null && el !== undefined) { if (!isNullOrUndefined(el)) {
this.videoGlobal = el.nativeElement; this.videoGlobal = el.nativeElement;
} }
} }
videoPlayer: HTMLVideoElement; videoPlayer: HTMLVideoElement;
@ViewChild('videoPlayer') @ViewChild('videoPlayer')
set mainVideoEl(el: ElementRef) { set mainVideoEl(el: ElementRef) {
if(el !== null && el !== undefined) { if (!isNullOrUndefined(el)) {
this.videoPlayer = el.nativeElement; this.videoPlayer = el.nativeElement;
} }
} }
videoCanva: any; videoCanva: any;
@ViewChild('canvascreenshoot') @ViewChild('canvascreenshoot')
set mainCanvaEl(el: ElementRef) { set mainCanvaEl(el: ElementRef) {
if(el !== null && el !== undefined) { if (!isNullOrUndefined(el)) {
this.videoCanva = el.nativeElement; this.videoCanva = el.nativeElement;
} }
} }
@ -73,12 +73,28 @@ export class VideoScene implements OnInit {
displayNeedHide: boolean = false; displayNeedHide: boolean = false;
timeLeft: number = 10; timeLeft: number = 10;
interval = null; interval = null;
userMetaData: UserMediaAdvancement = undefined
previousTime: number = 0;
startPlayingTime: number = undefined;
hasFullReadTheVideo: {
previousTime: number,
totalCount: number,
registered: boolean
} = {
previousTime: 0,
totalCount: 0,
registered: false
};
constructor(private videoService: VideoService, constructor(private videoService: VideoService,
private seriesService: SeriesService, private seriesService: SeriesService,
private seasonService: SeasonService, private seasonService: SeasonService,
private httpService: HttpWrapperService, private httpService: HttpWrapperService,
private arianeService: ArianeService, private arianeService: ArianeService,
private dataService: DataService) { private dataService: DataService,
private advancementService: AdvancementService) {
} }
@ -281,16 +297,28 @@ export class VideoScene implements OnInit {
self.mediaIsNotFound = true; self.mediaIsNotFound = true;
self.mediaIsLoading = false; self.mediaIsLoading = false;
}); });
this.advancementService.get(this.idVideo)
.then((response: UserMediaAdvancement) => {
this.userMetaData = response
this.startPlayingTime = response.time
})
} }
onRequirePlay() { onRequirePlay() {
this.startHideTimer(); this.startHideTimer();
this.playVideo = true; this.playVideo = true;
this.hasFullReadTheVideo = {
previousTime: 0,
totalCount: 0,
registered: false
};
this.displayVolumeMenu = false; this.displayVolumeMenu = false;
} }
onRequireStop() { onRequireStop() {
this.startHideTimer(); this.startHideTimer();
const startPlayingTime = this.videoPlayer.currentTime
this.playVideo = false; this.playVideo = false;
this.displayVolumeMenu = false; this.displayVolumeMenu = false;
this.startPlayingTime = startPlayingTime;
} }
onVideoEnded() { onVideoEnded() {
this.startHideTimer(); this.startHideTimer();
@ -336,17 +364,60 @@ export class VideoScene implements OnInit {
} }
return out; return out;
} }
configureDefaultValue(): boolean {
if (!isNullOrUndefined(this.startPlayingTime)) {
this.videoPlayer.currentTime = this.startPlayingTime
this.startPlayingTime = undefined;
return true;
}
return false;
}
changeTimeupdate(currentTime: any) { changeTimeupdate(currentTime: any) {
// console.log("time change "); // console.log("time change ");
// console.log(" ==> " + this.videoPlayer.currentTime); // console.log(" ==> " + this.videoPlayer.currentTime);
// Normal display part
this.currentTime = this.videoPlayer.currentTime; this.currentTime = this.videoPlayer.currentTime;
this.currentTimeDisplay = this.convertIndisplayTime(this.currentTime); this.currentTimeDisplay = this.convertIndisplayTime(this.currentTime);
// Cat set default value only at start
if (this.configureDefaultValue()) {
this.hasFullReadTheVideo.previousTime = this.currentTime;
return;
}
let needAddOneCount = false;
// Section to manage the detection of full read of the video
if (!this.hasFullReadTheVideo.registered) {
console.log(`Check !!!!!!!! ${this.currentTime} / ${this.hasFullReadTheVideo.previousTime} ==> ${this.currentTime - this.hasFullReadTheVideo.previousTime}`);
if (Math.abs(this.currentTime - this.hasFullReadTheVideo.previousTime) < 10) {
console.log(` ==> step 1`);
if (this.currentTime > this.hasFullReadTheVideo.previousTime) {
this.hasFullReadTheVideo.totalCount += this.currentTime - this.hasFullReadTheVideo.previousTime;
}
console.log(` ==> step 2 percent = ${this.currentTime / this.duration}, total=${this.hasFullReadTheVideo.totalCount / 60}`);
// Detect the passing of 90%
if (this.currentTime / this.duration > 0.9) {
console.log(` ==> step 3`);
this.hasFullReadTheVideo.registered = true;
// Add cont only if the user watching more than 3 minutes
if (this.hasFullReadTheVideo.totalCount > 60 * 3) {
console.log(` ==> step 4 ==> You win`);
needAddOneCount = true;
}
}
}
}
this.hasFullReadTheVideo.previousTime = this.currentTime;
// console.log(" ==> " + this.currentTimeDisplay); // console.log(" ==> " + this.currentTimeDisplay);
if (needAddOneCount || Math.abs(this.previousTime - this.currentTime) > 15) {
this.previousTime = this.currentTime;
this.advancementService.updateTime(this.idVideo, this.currentTime, this.duration, needAddOneCount);
}
} }
changeDurationchange(duration: any) { changeDurationchange(duration: any) {
console.log('duration change '); console.log('duration change ');
console.log(` ==> ${this.videoPlayer.duration}`); console.log(` ==> ${this.videoPlayer.duration}`);
this.configureDefaultValue();
this.duration = this.videoPlayer.duration; this.duration = this.videoPlayer.duration;
this.durationDisplay = this.convertIndisplayTime(this.duration); this.durationDisplay = this.convertIndisplayTime(this.duration);
} }

View File

@ -0,0 +1,96 @@
import { isNodeData, NodeData } from "common/model";
import { HttpWrapperService, BddService } from "common/service";
import { DataInterface, isArrayOf, isNullOrUndefined, TypeCheck } from "common/utils";
export class GenericInterfaceModelDBv2<TYPE> {
constructor(
protected serviceName: string,
protected http: HttpWrapperService,
protected bdd: BddService,
protected checker: (subData: any) => subData is TYPE) {
// nothing to do ...
}
gets(): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self.bdd.get(self.serviceName)
.then((response: DataInterface) => {
let data = response.gets();
if (isNullOrUndefined(data)) {
reject('Data does not exist in the local BDD');
return;
}
if (isArrayOf<TYPE>(data, this.checker)) {
resolve(data);
} else {
reject("The data is not an array of the correct type ...");
}
return;
}).catch((response) => {
reject(response);
});
});
}
get(id: number): Promise<TYPE> {
let self = this;
return new Promise((resolve, reject) => {
self.bdd.get(self.serviceName)
.then((response: DataInterface) => {
let data = response.get(id);
if (isNullOrUndefined(data)) {
reject('Data does not exist in the local BDD');
return;
}
if (this.checker(data)) {
resolve(data);
} else {
reject("The data is not an array of the correct type ...");
}
return;
}).catch((response) => {
reject(response);
});
});
}
getFilter(filter: (subData: any) => boolean): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self.bdd.get(self.serviceName)
.then((response: DataInterface) => {
let data = response.gets();
if (isNullOrUndefined(data)) {
reject('Data does not exist in the local BDD');
return;
}
let out = [];
for (const elem of data) {
if (filter(elem)) {
out.push(elem);
}
}
resolve(out as TYPE[]);
return;
}).catch((response) => {
reject(response);
});
});
}
insert(data: object): TYPE {
let ret = this.http.postSpecific([this.serviceName], data);
return this.bdd.addAfterPost(this.serviceName, ret) as TYPE;
}
put(id: number, data: object): TYPE {
let ret = this.http.putSpecific([this.serviceName, id], data);
return this.bdd.setAfterPut(this.serviceName, id, ret) as TYPE;
}
delete(id: number): TYPE {
let ret = this.http.deleteSpecific([this.serviceName, id]);
return this.bdd.delete(this.serviceName, id, ret) as TYPE;
}
}

View File

@ -0,0 +1,39 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Injectable } from '@angular/core';
import { HttpWrapperService, BddService } from 'common/service';
import { isUserMediaAdvancement, UserMediaAdvancement } from 'app/model/user-media-advancement';
import { GenericInterfaceModelDBv2 } from './GenericInterfaceModelDBv2';
@Injectable()
export class AdvancementService extends GenericInterfaceModelDBv2<UserMediaAdvancement> {
constructor(http: HttpWrapperService,
bdd: BddService) {
super('advancement', http, bdd, isUserMediaAdvancement);
}
get(id: number): Promise<UserMediaAdvancement> {
return new Promise((resolve, reject) => {
super.getFilter((data: any) => { return data.mediaId == id }).then((data: UserMediaAdvancement[]) => {
resolve(data[0]);
return;
}).catch((reason: any) => {
reject(reason);
});
});
}
updateTime(id: number, time: number, total: number, addCount: boolean) {
this.put(id, {
time: time,
percent: time / total,
addCount: addCount
});
}
}

View File

@ -1,3 +1,4 @@
import { AdvancementService } from "./advancement";
import { ArianeService } from "./ariane"; import { ArianeService } from "./ariane";
import { DataService } from "./data"; import { DataService } from "./data";
import { SeasonService } from "./season"; import { SeasonService } from "./season";
@ -14,6 +15,7 @@ export {
SeriesService, SeriesService,
TypeService, TypeService,
VideoService, VideoService,
AdvancementService
}; };

@ -1 +1 @@
Subproject commit 9fc25b4feaeba509ff39f70b24d97be47f4b30e1 Subproject commit ea5a4f6b7537eb707916f4610bf79fbe86c6296f