From 1942f11d5ee6926ad93dc1b79fae18325ba5de18 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:49:20 +0200 Subject: Lazy load all routes --- .../comment/video-comment-add.component.html | 56 -- .../comment/video-comment-add.component.scss | 82 --- .../comment/video-comment-add.component.ts | 149 ---- .../comment/video-comment-thread-tree.model.ts | 7 - .../comment/video-comment.component.html | 95 --- .../comment/video-comment.component.scss | 189 ----- .../comment/video-comment.component.ts | 131 ---- .../+video-watch/comment/video-comment.model.ts | 48 -- .../+video-watch/comment/video-comment.service.ts | 149 ---- .../comment/video-comments.component.html | 98 --- .../comment/video-comments.component.scss | 53 -- .../comment/video-comments.component.ts | 232 ------ .../+video-watch/modal/video-share.component.html | 187 ----- .../+video-watch/modal/video-share.component.scss | 79 --- .../+video-watch/modal/video-share.component.ts | 126 ---- .../modal/video-support.component.html | 15 - .../modal/video-support.component.scss | 3 - .../+video-watch/modal/video-support.component.ts | 29 - .../timestamp-route-transformer.directive.ts | 39 - .../+video-watch/video-duration-formatter.pipe.ts | 28 - .../video-watch-playlist.component.html | 46 -- .../video-watch-playlist.component.scss | 83 --- .../+video-watch/video-watch-playlist.component.ts | 201 ------ .../+video-watch/video-watch-routing.module.ts | 27 - .../videos/+video-watch/video-watch.component.html | 277 -------- .../videos/+video-watch/video-watch.component.scss | 607 ---------------- .../videos/+video-watch/video-watch.component.ts | 782 --------------------- .../app/videos/+video-watch/video-watch.module.ts | 65 -- 28 files changed, 3883 deletions(-) delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment-add.component.html delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment-add.component.scss delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment-add.component.ts delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment.component.html delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment.component.scss delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment.component.ts delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment.model.ts delete mode 100644 client/src/app/videos/+video-watch/comment/video-comment.service.ts delete mode 100644 client/src/app/videos/+video-watch/comment/video-comments.component.html delete mode 100644 client/src/app/videos/+video-watch/comment/video-comments.component.scss delete mode 100644 client/src/app/videos/+video-watch/comment/video-comments.component.ts delete mode 100644 client/src/app/videos/+video-watch/modal/video-share.component.html delete mode 100644 client/src/app/videos/+video-watch/modal/video-share.component.scss delete mode 100644 client/src/app/videos/+video-watch/modal/video-share.component.ts delete mode 100644 client/src/app/videos/+video-watch/modal/video-support.component.html delete mode 100644 client/src/app/videos/+video-watch/modal/video-support.component.scss delete mode 100644 client/src/app/videos/+video-watch/modal/video-support.component.ts delete mode 100644 client/src/app/videos/+video-watch/timestamp-route-transformer.directive.ts delete mode 100644 client/src/app/videos/+video-watch/video-duration-formatter.pipe.ts delete mode 100644 client/src/app/videos/+video-watch/video-watch-playlist.component.html delete mode 100644 client/src/app/videos/+video-watch/video-watch-playlist.component.scss delete mode 100644 client/src/app/videos/+video-watch/video-watch-playlist.component.ts delete mode 100644 client/src/app/videos/+video-watch/video-watch-routing.module.ts delete mode 100644 client/src/app/videos/+video-watch/video-watch.component.html delete mode 100644 client/src/app/videos/+video-watch/video-watch.component.scss delete mode 100644 client/src/app/videos/+video-watch/video-watch.component.ts delete mode 100644 client/src/app/videos/+video-watch/video-watch.module.ts (limited to 'client/src/app/videos/+video-watch') diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html deleted file mode 100644 index 9b43d91da..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html +++ /dev/null @@ -1,56 +0,0 @@ -
-
- Avatar - -
- -
- {{ formErrors.text }} -
-
-
- -
- - -
-
- - - - - - diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss deleted file mode 100644 index b3725ab94..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss +++ /dev/null @@ -1,82 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -form { - margin-bottom: 30px; -} - -.avatar-and-textarea { - display: flex; - margin-bottom: 10px; - - img { - @include avatar(25px); - - vertical-align: top; - margin-right: 10px; - } - - .form-group { - flex-grow: 1; - margin: 0; - - textarea { - @include peertube-textarea(100%, 60px); - - &:focus::placeholder { - opacity: 0; - } - } - } -} - -.comment-buttons { - display: flex; - justify-content: flex-end; - - button { - @include peertube-button; - @include disable-outline; - @include disable-default-a-behaviour; - - &:not(:last-child) { - margin-right: .5rem; - } - - &:last-child { - @include orange-button; - } - } - - .cancel-button { - @include tertiary-button; - - font-weight: $font-semibold; - display: inline-block; - padding: 0 10px 0 10px; - white-space: nowrap; - background: transparent; - } -} - -@media screen and (max-width: 600px) { - textarea, .comment-buttons button { - font-size: 14px !important; - } - - textarea { - padding: 5px !important; - } -} - -.modal-body { - .btn { - @include peertube-button; - @include orange-button; - } - - span { - float: left; - margin-bottom: 20px; - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts deleted file mode 100644 index 79505c779..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Observable } from 'rxjs' -import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' -import { Router } from '@angular/router' -import { Notifier, User } from '@app/core' -import { FormReactive, FormValidatorService, VideoCommentValidatorsService } from '@app/shared/shared-forms' -import { Video } from '@app/shared/shared-main' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { VideoCommentCreate } from '@shared/models' -import { VideoComment } from './video-comment.model' -import { VideoCommentService } from './video-comment.service' - -@Component({ - selector: 'my-video-comment-add', - templateUrl: './video-comment-add.component.html', - styleUrls: ['./video-comment-add.component.scss'] -}) -export class VideoCommentAddComponent extends FormReactive implements OnInit { - @Input() user: User - @Input() video: Video - @Input() parentComment: VideoComment - @Input() parentComments: VideoComment[] - @Input() focusOnInit = false - - @Output() commentCreated = new EventEmitter() - @Output() cancel = new EventEmitter() - - @ViewChild('visitorModal', { static: true }) visitorModal: NgbModal - @ViewChild('textarea', { static: true }) textareaElement: ElementRef - - addingComment = false - - constructor ( - protected formValidatorService: FormValidatorService, - private videoCommentValidatorsService: VideoCommentValidatorsService, - private notifier: Notifier, - private videoCommentService: VideoCommentService, - private modalService: NgbModal, - private router: Router - ) { - super() - } - - ngOnInit () { - this.buildForm({ - text: this.videoCommentValidatorsService.VIDEO_COMMENT_TEXT - }) - - if (this.user) { - if (this.focusOnInit === true) { - this.textareaElement.nativeElement.focus() - } - - if (this.parentComment) { - const mentions = this.parentComments - .filter(c => c.account && c.account.id !== this.user.account.id) // Don't add mention of ourselves - .map(c => '@' + c.by) - - const mentionsSet = new Set(mentions) - const mentionsText = Array.from(mentionsSet).join(' ') + ' ' - - this.form.patchValue({ text: mentionsText }) - } - } - } - - onValidKey () { - this.check() - if (!this.form.valid) return - - this.formValidated() - } - - openVisitorModal (event: any) { - if (this.user === null) { // we only open it for visitors - // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error - event.srcElement.blur() - event.preventDefault() - - this.modalService.open(this.visitorModal) - } - } - - hideVisitorModal () { - this.modalService.dismissAll() - } - - formValidated () { - // If we validate very quickly the comment form, we might comment twice - if (this.addingComment) return - - this.addingComment = true - - const commentCreate: VideoCommentCreate = this.form.value - let obs: Observable - - if (this.parentComment) { - obs = this.addCommentReply(commentCreate) - } else { - obs = this.addCommentThread(commentCreate) - } - - obs.subscribe( - comment => { - this.addingComment = false - this.commentCreated.emit(comment) - this.form.reset() - }, - - err => { - this.addingComment = false - - this.notifier.error(err.text) - } - ) - } - - isAddButtonDisplayed () { - return this.form.value['text'] - } - - getUri () { - return window.location.href - } - - getAvatarUrl () { - if (this.user) return this.user.accountAvatarUrl - return window.location.origin + '/client/assets/images/default-avatar.png' - } - - gotoLogin () { - this.hideVisitorModal() - this.router.navigate([ '/login' ]) - } - - cancelCommentReply () { - this.cancel.emit(null) - this.form.value['text'] = this.textareaElement.nativeElement.value = '' - } - - private addCommentReply (commentCreate: VideoCommentCreate) { - return this.videoCommentService - .addCommentReply(this.video.id, this.parentComment.id, commentCreate) - } - - private addCommentThread (commentCreate: VideoCommentCreate) { - return this.videoCommentService - .addCommentThread(this.video.id, commentCreate) - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts b/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts deleted file mode 100644 index 7c2aaeadd..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment-thread-tree.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { VideoCommentThreadTree as VideoCommentThreadTreeServerModel } from '@shared/models' -import { VideoComment } from './video-comment.model' - -export class VideoCommentThreadTree implements VideoCommentThreadTreeServerModel { - comment: VideoComment - children: VideoCommentThreadTree[] -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.html b/client/src/app/videos/+video-watch/comment/video-comment.component.html deleted file mode 100644 index 002de57e4..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.html +++ /dev/null @@ -1,95 +0,0 @@ -
-
- - Avatar - - -
-
- -
- - -
- -
Highlighted comment
- - -
- -
-
Reply
-
Delete
- - -
-
- - - - -
- This comment has been deleted -
-
- - - -
-
- -
-
- - -
-
-
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss deleted file mode 100644 index e7ef79561..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss +++ /dev/null @@ -1,189 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.root-comment { - font-size: 15px; - display: flex; - - .left { - display: flex; - flex-direction: column; - align-items: center; - margin-right: 10px; - - .vertical-border { - width: 2px; - height: 100%; - background-color: rgba(0, 0, 0, 0.05); - margin: 10px calc(1rem + 1px); - } - } - - .right { - width: 100%; - } - - .comment-avatar { - @include avatar(36px); - } - - .comment { - flex-grow: 1; - // Fix word-wrap with flex - min-width: 1px; - - .highlighted-comment { - display: inline-block; - background-color: #F5F5F5; - color: #3d3d3d; - padding: 0 5px; - font-size: 13px; - margin-bottom: 5px; - font-weight: $font-semibold; - border-radius: 3px; - } - - .comment-account-date { - display: flex; - margin-bottom: 4px; - - .video-author { - height: 20px; - background-color: #888888; - border-radius: 12px; - margin-bottom: 2px; - max-width: 100%; - box-sizing: border-box; - flex-direction: row; - align-items: center; - display: inline-flex; - padding-right: 6px; - padding-left: 6px; - color: white !important; - } - - .comment-account { - word-break: break-all; - font-weight: 600; - font-size: 90%; - - a { - @include disable-default-a-behaviour; - - color: pvar(--mainForegroundColor); - } - - .comment-account-fid { - opacity: .6; - } - } - - .comment-date { - font-size: 90%; - color: pvar(--greyForegroundColor); - margin-left: 5px; - text-decoration: none; - } - } - - .comment-html { - @include peertube-word-wrap; - - // Mentions - ::ng-deep a { - - &:not(.linkified-url) { - @include disable-default-a-behaviour; - - color: pvar(--mainForegroundColor); - - font-weight: $font-semibold; - } - - } - - // Paragraphs - ::ng-deep p { - margin-bottom: .3rem; - } - - &.comment-html-deleted { - color: pvar(--greyForegroundColor); - margin-bottom: 1rem; - } - } - - .comment-actions { - margin-bottom: 10px; - display: flex; - - ::ng-deep .dropdown-toggle, - .comment-action-reply, - .comment-action-delete { - color: pvar(--greyForegroundColor); - cursor: pointer; - margin-right: 10px; - - &:hover { - color: pvar(--mainForegroundColor); - } - } - - ::ng-deep .action-button { - background-color: transparent; - padding: 0; - font-weight: unset; - } - } - - my-video-comment-add { - ::ng-deep form { - margin-top: 1rem; - margin-bottom: 0; - } - } - } - - .children { - // Reduce avatars size for replies - .comment-avatar { - @include avatar(25px); - } - - .left { - margin-right: 6px; - } - } -} - -@media screen and (max-width: 1200px) { - .children { - margin-left: -10px; - } -} - -@media screen and (max-width: 600px) { - .root-comment { - .children { - margin-left: -20px; - - .left { - align-items: flex-start; - - .vertical-border { - margin-left: 2px; - } - } - } - - .comment { - .comment-account-date { - flex-direction: column; - - .comment-date { - margin-left: 0; - } - } - } - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts deleted file mode 100644 index 27846c1ad..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' -import { MarkdownService, Notifier, UserService } from '@app/core' -import { AuthService } from '@app/core/auth' -import { Account, Actor, Video } from '@app/shared/shared-main' -import { User, UserRight } from '@shared/models' -import { VideoCommentThreadTree } from './video-comment-thread-tree.model' -import { VideoComment } from './video-comment.model' - -@Component({ - selector: 'my-video-comment', - templateUrl: './video-comment.component.html', - styleUrls: ['./video-comment.component.scss'] -}) -export class VideoCommentComponent implements OnInit, OnChanges { - @Input() video: Video - @Input() comment: VideoComment - @Input() parentComments: VideoComment[] = [] - @Input() commentTree: VideoCommentThreadTree - @Input() inReplyToCommentId: number - @Input() highlightedComment = false - @Input() firstInThread = false - - @Output() wantedToDelete = new EventEmitter() - @Output() wantedToReply = new EventEmitter() - @Output() threadCreated = new EventEmitter() - @Output() resetReply = new EventEmitter() - @Output() timestampClicked = new EventEmitter() - - sanitizedCommentHTML = '' - newParentComments: VideoComment[] = [] - - commentAccount: Account - commentUser: User - - constructor ( - private markdownService: MarkdownService, - private authService: AuthService, - private userService: UserService, - private notifier: Notifier - ) {} - - get user () { - return this.authService.getUser() - } - - ngOnInit () { - this.init() - } - - ngOnChanges () { - this.init() - } - - onCommentReplyCreated (createdComment: VideoComment) { - if (!this.commentTree) { - this.commentTree = { - comment: this.comment, - children: [] - } - - this.threadCreated.emit(this.commentTree) - } - - this.commentTree.children.unshift({ - comment: createdComment, - children: [] - }) - this.resetReply.emit() - } - - onWantToReply (comment?: VideoComment) { - this.wantedToReply.emit(comment || this.comment) - } - - onWantToDelete (comment?: VideoComment) { - this.wantedToDelete.emit(comment || this.comment) - } - - isUserLoggedIn () { - return this.authService.isLoggedIn() - } - - onResetReply () { - this.resetReply.emit() - } - - handleTimestampClicked (timestamp: number) { - this.timestampClicked.emit(timestamp) - } - - isRemovableByUser () { - return this.comment.account && this.isUserLoggedIn() && - ( - this.user.account.id === this.comment.account.id || - this.user.account.id === this.video.account.id || - this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) - ) - } - - switchToDefaultAvatar ($event: Event) { - ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() - } - - private getUserIfNeeded (account: Account) { - if (!account.userId) return - if (!this.authService.isLoggedIn()) return - - const user = this.authService.getUser() - if (user.hasRight(UserRight.MANAGE_USERS)) { - this.userService.getUserWithCache(account.userId) - .subscribe( - user => this.commentUser = user, - - err => this.notifier.error(err.message) - ) - } - } - - private async init () { - const html = await this.markdownService.textMarkdownToHTML(this.comment.text, true) - this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html) - this.newParentComments = this.parentComments.concat([ this.comment ]) - - if (this.comment.account) { - this.commentAccount = new Account(this.comment.account) - this.getUserIfNeeded(this.commentAccount) - } else { - this.comment.account = null - } - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts deleted file mode 100644 index e85443196..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment.model.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { getAbsoluteAPIUrl } from '@app/helpers' -import { Actor } from '@app/shared/shared-main' -import { Account as AccountInterface, VideoComment as VideoCommentServerModel } from '@shared/models' - -export class VideoComment implements VideoCommentServerModel { - id: number - url: string - text: string - threadId: number - inReplyToCommentId: number - videoId: number - createdAt: Date | string - updatedAt: Date | string - deletedAt: Date | string - isDeleted: boolean - account: AccountInterface - totalRepliesFromVideoAuthor: number - totalReplies: number - by: string - accountAvatarUrl: string - - isLocal: boolean - - constructor (hash: VideoCommentServerModel) { - this.id = hash.id - this.url = hash.url - this.text = hash.text - this.threadId = hash.threadId - this.inReplyToCommentId = hash.inReplyToCommentId - this.videoId = hash.videoId - this.createdAt = new Date(hash.createdAt.toString()) - this.updatedAt = new Date(hash.updatedAt.toString()) - this.deletedAt = hash.deletedAt ? new Date(hash.deletedAt.toString()) : null - this.isDeleted = hash.isDeleted - this.account = hash.account - this.totalRepliesFromVideoAuthor = hash.totalRepliesFromVideoAuthor - this.totalReplies = hash.totalReplies - - if (this.account) { - this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) - this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) - - const absoluteAPIUrl = getAbsoluteAPIUrl() - const thisHost = new URL(absoluteAPIUrl).host - this.isLocal = this.account.host.trim() === thisHost - } - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts deleted file mode 100644 index a73fb9ca8..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Observable } from 'rxjs' -import { catchError, map } from 'rxjs/operators' -import { HttpClient, HttpParams } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' -import { objectLineFeedToHtml } from '@app/helpers' -import { - FeedFormat, - ResultList, - VideoComment as VideoCommentServerModel, - VideoCommentCreate, - VideoCommentThreadTree as VideoCommentThreadTreeServerModel -} from '@shared/models' -import { environment } from '../../../../environments/environment' -import { VideoCommentThreadTree } from './video-comment-thread-tree.model' -import { VideoComment } from './video-comment.model' - -@Injectable() -export class VideoCommentService { - private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' - private static BASE_FEEDS_URL = environment.apiUrl + '/feeds/video-comments.' - - constructor ( - private authHttp: HttpClient, - private restExtractor: RestExtractor, - private restService: RestService - ) {} - - addCommentThread (videoId: number | string, comment: VideoCommentCreate) { - const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' - const normalizedComment = objectLineFeedToHtml(comment, 'text') - - return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) - .pipe( - map(data => this.extractVideoComment(data.comment)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { - const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId - const normalizedComment = objectLineFeedToHtml(comment, 'text') - - return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) - .pipe( - map(data => this.extractVideoComment(data.comment)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getVideoCommentThreads (parameters: { - videoId: number | string, - componentPagination: ComponentPaginationLight, - sort: string - }): Observable> { - const { videoId, componentPagination, sort } = parameters - - const pagination = this.restService.componentPaginationToRestPagination(componentPagination) - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' - return this.authHttp.get>(url, { params }) - .pipe( - map(result => this.extractVideoComments(result)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getVideoThreadComments (parameters: { - videoId: number | string, - threadId: number - }): Observable { - const { videoId, threadId } = parameters - const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` - - return this.authHttp - .get(url) - .pipe( - map(tree => this.extractVideoCommentTree(tree)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - deleteVideoComment (videoId: number | string, commentId: number) { - const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comments/${commentId}` - - return this.authHttp - .delete(url) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getVideoCommentsFeeds (videoUUID?: string) { - const feeds = [ - { - format: FeedFormat.RSS, - label: 'rss 2.0', - url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase() - }, - { - format: FeedFormat.ATOM, - label: 'atom 1.0', - url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase() - }, - { - format: FeedFormat.JSON, - label: 'json 1.0', - url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() - } - ] - - if (videoUUID !== undefined) { - for (const feed of feeds) { - feed.url += '?videoId=' + videoUUID - } - } - - return feeds - } - - private extractVideoComment (videoComment: VideoCommentServerModel) { - return new VideoComment(videoComment) - } - - private extractVideoComments (result: ResultList) { - const videoCommentsJson = result.data - const totalComments = result.total - const comments: VideoComment[] = [] - - for (const videoCommentJson of videoCommentsJson) { - comments.push(new VideoComment(videoCommentJson)) - } - - return { data: comments, total: totalComments } - } - - private extractVideoCommentTree (tree: VideoCommentThreadTreeServerModel) { - if (!tree) return tree as VideoCommentThreadTree - - tree.comment = new VideoComment(tree.comment) - tree.children.forEach(c => this.extractVideoCommentTree(c)) - - return tree as VideoCommentThreadTree - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html deleted file mode 100644 index dd1d43560..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.html +++ /dev/null @@ -1,98 +0,0 @@ -
-
-

- - - 1 Comment - {{ componentPagination.totalItems }} Comments - - Comments -

- - - -
- -
- - -
-
-
- - - - -
No comments.
- -
-
-
- -
- -
- -
- - - - - - View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} and others - - - View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} - - - View {{ comment.totalReplies }} replies - - -
-
- -
-
-
- -
- Comments are disabled. -
-
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss deleted file mode 100644 index df42fae73..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss +++ /dev/null @@ -1,53 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -#highlighted-comment { - margin-bottom: 25px; -} - -.view-replies { - font-weight: $font-semibold; - font-size: 15px; - cursor: pointer; -} - -.glyphicon, .comment-thread-loading { - margin-right: 5px; - display: inline-block; - font-size: 13px; -} - -.title-block { - .title-page { - margin-right: 0; - } - - my-feed { - display: inline-block; - margin-left: 5px; - opacity: 0; - transition: ease-in .2s opacity; - } - &:hover my-feed { - opacity: 1; - } -} - -#dropdown-sort-comments { - font-weight: 600; - text-transform: uppercase; - border: none; - transform: translateY(-7%); -} - -@media screen and (max-width: 600px) { - .view-replies { - margin-left: 46px; - } -} - -@media screen and (max-width: 450px) { - .view-replies { - font-size: 14px; - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts deleted file mode 100644 index df0018ec6..000000000 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { Subject, Subscription } from 'rxjs' -import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, User } from '@app/core' -import { HooksService } from '@app/core/plugins/hooks.service' -import { Syndication, VideoDetails } from '@app/shared/shared-main' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoCommentThreadTree } from './video-comment-thread-tree.model' -import { VideoComment } from './video-comment.model' -import { VideoCommentService } from './video-comment.service' - -@Component({ - selector: 'my-video-comments', - templateUrl: './video-comments.component.html', - styleUrls: ['./video-comments.component.scss'] -}) -export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { - @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef - @Input() video: VideoDetails - @Input() user: User - - @Output() timestampClicked = new EventEmitter() - - comments: VideoComment[] = [] - highlightedThread: VideoComment - sort = '-createdAt' - componentPagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 10, - totalItems: null - } - inReplyToCommentId: number - threadComments: { [ id: number ]: VideoCommentThreadTree } = {} - threadLoading: { [ id: number ]: boolean } = {} - - syndicationItems: Syndication[] = [] - - onDataSubject = new Subject() - - private sub: Subscription - - constructor ( - private authService: AuthService, - private notifier: Notifier, - private confirmService: ConfirmService, - private videoCommentService: VideoCommentService, - private activatedRoute: ActivatedRoute, - private i18n: I18n, - private hooks: HooksService - ) {} - - ngOnInit () { - // Find highlighted comment in params - this.sub = this.activatedRoute.params.subscribe( - params => { - if (params['threadId']) { - const highlightedThreadId = +params['threadId'] - this.processHighlightedThread(highlightedThreadId) - } - } - ) - } - - ngOnChanges (changes: SimpleChanges) { - if (changes['video']) { - this.resetVideo() - } - } - - ngOnDestroy () { - if (this.sub) this.sub.unsubscribe() - } - - viewReplies (commentId: number, highlightThread = false) { - this.threadLoading[commentId] = true - - const params = { - videoId: this.video.id, - threadId: commentId - } - - const obs = this.hooks.wrapObsFun( - this.videoCommentService.getVideoThreadComments.bind(this.videoCommentService), - params, - 'video-watch', - 'filter:api.video-watch.video-thread-replies.list.params', - 'filter:api.video-watch.video-thread-replies.list.result' - ) - - obs.subscribe( - res => { - this.threadComments[commentId] = res - this.threadLoading[commentId] = false - this.hooks.runAction('action:video-watch.video-thread-replies.loaded', 'video-watch', { data: res }) - - if (highlightThread) { - this.highlightedThread = new VideoComment(res.comment) - - // Scroll to the highlighted thread - setTimeout(() => this.commentHighlightBlock.nativeElement.scrollIntoView(), 0) - } - }, - - err => this.notifier.error(err.message) - ) - } - - loadMoreThreads () { - const params = { - videoId: this.video.id, - componentPagination: this.componentPagination, - sort: this.sort - } - - const obs = this.hooks.wrapObsFun( - this.videoCommentService.getVideoCommentThreads.bind(this.videoCommentService), - params, - 'video-watch', - 'filter:api.video-watch.video-threads.list.params', - 'filter:api.video-watch.video-threads.list.result' - ) - - obs.subscribe( - res => { - this.comments = this.comments.concat(res.data) - this.componentPagination.totalItems = res.total - - this.onDataSubject.next(res.data) - this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination }) - }, - - err => this.notifier.error(err.message) - ) - } - - onCommentThreadCreated (comment: VideoComment) { - this.comments.unshift(comment) - } - - onWantedToReply (comment: VideoComment) { - this.inReplyToCommentId = comment.id - } - - onResetReply () { - this.inReplyToCommentId = undefined - } - - onThreadCreated (commentTree: VideoCommentThreadTree) { - this.viewReplies(commentTree.comment.id) - } - - handleSortChange (sort: string) { - if (this.sort === sort) return - - this.sort = sort - this.resetVideo() - } - - handleTimestampClicked (timestamp: number) { - this.timestampClicked.emit(timestamp) - } - - async onWantedToDelete (commentToDelete: VideoComment) { - let message = 'Do you really want to delete this comment?' - - if (commentToDelete.isLocal || this.video.isLocal) { - message += this.i18n(' The deletion will be sent to remote instances so they can reflect the change.') - } else { - message += this.i18n(' It is a remote comment, so the deletion will only be effective on your instance.') - } - - const res = await this.confirmService.confirm(message, this.i18n('Delete')) - if (res === false) return - - this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id) - .subscribe( - () => { - if (this.highlightedThread?.id === commentToDelete.id) { - commentToDelete = this.comments.find(c => c.id === commentToDelete.id) - - this.highlightedThread = undefined - } - - // Mark the comment as deleted - this.softDeleteComment(commentToDelete) - }, - - err => this.notifier.error(err.message) - ) - } - - isUserLoggedIn () { - return this.authService.isLoggedIn() - } - - onNearOfBottom () { - if (hasMoreItems(this.componentPagination)) { - this.componentPagination.currentPage++ - this.loadMoreThreads() - } - } - - private softDeleteComment (comment: VideoComment) { - comment.isDeleted = true - comment.deletedAt = new Date() - comment.text = '' - comment.account = null - } - - private resetVideo () { - if (this.video.commentsEnabled === true) { - // Reset all our fields - this.highlightedThread = null - this.comments = [] - this.threadComments = {} - this.threadLoading = {} - this.inReplyToCommentId = undefined - this.componentPagination.currentPage = 1 - this.componentPagination.totalItems = null - - this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid) - this.loadMoreThreads() - } - } - - private processHighlightedThread (highlightedThreadId: number) { - this.highlightedThread = this.comments.find(c => c.id === highlightedThreadId) - - const highlightThread = true - this.viewReplies(highlightedThreadId, highlightThread) - } -} diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html deleted file mode 100644 index 5e6a2d518..000000000 --- a/client/src/app/videos/+video-watch/modal/video-share.component.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.scss b/client/src/app/videos/+video-watch/modal/video-share.component.scss deleted file mode 100644 index 091d4dc3b..000000000 --- a/client/src/app/videos/+video-watch/modal/video-share.component.scss +++ /dev/null @@ -1,79 +0,0 @@ -@import '_mixins'; -@import '_variables'; - -my-input-readonly-copy { - width: 100%; -} - -.title-page.title-page-single { - margin-top: 0; -} - -.playlist { - margin-bottom: 50px; -} - -.peertube-select-container { - @include peertube-select-container(200px); -} - -.qr-code-group { - text-align: center; -} - -.nav-content { - margin-top: 30px; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.alert { - margin-top: 20px; -} - -.filters { - margin-top: 30px; - - .advanced-filters-button { - display: flex; - justify-content: center; - align-items: center; - margin-top: 20px; - font-size: 16px; - font-weight: $font-semibold; - cursor: pointer; - - .glyphicon { - margin-right: 5px; - } - } - - .form-group { - margin-bottom: 0; - height: 34px; - display: flex; - align-items: center; - } - - .video-caption-block { - display: flex; - align-items: center; - - .peertube-select-container { - margin-left: 10px; - } - } - - .start-at, - .stop-at { - width: 300px; - display: flex; - align-items: center; - - my-timestamp-input { - margin-left: 10px; - } - } -} diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts deleted file mode 100644 index b42b775c1..000000000 --- a/client/src/app/videos/+video-watch/modal/video-share.component.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Component, ElementRef, Input, ViewChild } from '@angular/core' -import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { VideoCaption } from '@shared/models' -import { VideoDetails } from '@app/shared/shared-main' -import { VideoPlaylist } from '@app/shared/shared-video-playlist' - -type Customizations = { - startAtCheckbox: boolean - startAt: number - - stopAtCheckbox: boolean - stopAt: number - - subtitleCheckbox: boolean - subtitle: string - - loop: boolean - autoplay: boolean - muted: boolean - title: boolean - warningTitle: boolean - controls: boolean -} - -@Component({ - selector: 'my-video-share', - templateUrl: './video-share.component.html', - styleUrls: [ './video-share.component.scss' ] -}) -export class VideoShareComponent { - @ViewChild('modal', { static: true }) modal: ElementRef - - @Input() video: VideoDetails = null - @Input() videoCaptions: VideoCaption[] = [] - @Input() playlist: VideoPlaylist = null - - activeId: 'url' | 'qrcode' | 'embed' = 'url' - customizations: Customizations - isAdvancedCustomizationCollapsed = true - includeVideoInPlaylist = false - - constructor (private modalService: NgbModal) { } - - show (currentVideoTimestamp?: number) { - let subtitle: string - if (this.videoCaptions.length !== 0) { - subtitle = this.videoCaptions[0].language.id - } - - this.customizations = { - startAtCheckbox: false, - startAt: currentVideoTimestamp ? Math.floor(currentVideoTimestamp) : 0, - - stopAtCheckbox: false, - stopAt: this.video.duration, - - subtitleCheckbox: false, - subtitle, - - loop: false, - autoplay: false, - muted: false, - - // Embed options - title: true, - warningTitle: true, - controls: true - } - - this.modalService.open(this.modal, { centered: true }) - } - - getVideoIframeCode () { - const options = this.getOptions(this.video.embedUrl) - - const embedUrl = buildVideoLink(options) - return buildVideoEmbed(embedUrl) - } - - getVideoUrl () { - const baseUrl = window.location.origin + '/videos/watch/' + this.video.uuid - const options = this.getOptions(baseUrl) - - return buildVideoLink(options) - } - - getPlaylistUrl () { - const base = window.location.origin + '/videos/watch/playlist/' + this.playlist.uuid - - if (!this.includeVideoInPlaylist) return base - - return base + '?videoId=' + this.video.uuid - } - - notSecure () { - return window.location.protocol === 'http:' - } - - isInEmbedTab () { - return this.activeId === 'embed' - } - - hasPlaylist () { - return !!this.playlist - } - - private getOptions (baseUrl?: string) { - return { - baseUrl, - - startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined, - stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined, - - subtitle: this.customizations.subtitleCheckbox ? this.customizations.subtitle : undefined, - - loop: this.customizations.loop, - autoplay: this.customizations.autoplay, - muted: this.customizations.muted, - - title: this.customizations.title, - warningTitle: this.customizations.warningTitle, - controls: this.customizations.controls - } - } -} diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html deleted file mode 100644 index 935656d23..000000000 --- a/client/src/app/videos/+video-watch/modal/video-support.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.scss b/client/src/app/videos/+video-watch/modal/video-support.component.scss deleted file mode 100644 index 184e09027..000000000 --- a/client/src/app/videos/+video-watch/modal/video-support.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.action-button-cancel { - margin-right: 0 !important; -} diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts deleted file mode 100644 index 48d5f2948..000000000 --- a/client/src/app/videos/+video-watch/modal/video-support.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core' -import { MarkdownService } from '@app/core' -import { VideoDetails } from '@app/shared/shared-main' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' - -@Component({ - selector: 'my-video-support', - templateUrl: './video-support.component.html', - styleUrls: [ './video-support.component.scss' ] -}) -export class VideoSupportComponent { - @Input() video: VideoDetails = null - - @ViewChild('modal', { static: true }) modal: NgbModal - - videoHTMLSupport = '' - - constructor ( - private markdownService: MarkdownService, - private modalService: NgbModal - ) { } - - show () { - this.modalService.open(this.modal, { centered: true }) - - this.markdownService.enhancedMarkdownToHTML(this.video.support) - .then(r => this.videoHTMLSupport = r) - } -} diff --git a/client/src/app/videos/+video-watch/timestamp-route-transformer.directive.ts b/client/src/app/videos/+video-watch/timestamp-route-transformer.directive.ts deleted file mode 100644 index 45e023695..000000000 --- a/client/src/app/videos/+video-watch/timestamp-route-transformer.directive.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Directive, EventEmitter, HostListener, Output } from '@angular/core' - -@Directive({ - selector: '[timestampRouteTransformer]' -}) -export class TimestampRouteTransformerDirective { - @Output() timestampClicked = new EventEmitter() - - @HostListener('click', ['$event']) - public onClick ($event: Event) { - const target = $event.target as HTMLLinkElement - - if (target.hasAttribute('href') !== true) return - - const ngxLink = document.createElement('a') - ngxLink.href = target.getAttribute('href') - - // we only care about reflective links - if (ngxLink.host !== window.location.host) return - - const ngxLinkParams = new URLSearchParams(ngxLink.search) - if (ngxLinkParams.has('start') !== true) return - - const separators = ['h', 'm', 's'] - const start = ngxLinkParams - .get('start') - .match(new RegExp('(\\d{1,9}[' + separators.join('') + '])','g')) // match digits before any given separator - .map(t => { - if (t.includes('h')) return parseInt(t, 10) * 3600 - if (t.includes('m')) return parseInt(t, 10) * 60 - return parseInt(t, 10) - }) - .reduce((acc, t) => acc + t) - - this.timestampClicked.emit(start) - - $event.preventDefault() - } -} diff --git a/client/src/app/videos/+video-watch/video-duration-formatter.pipe.ts b/client/src/app/videos/+video-watch/video-duration-formatter.pipe.ts deleted file mode 100644 index 4b6767415..000000000 --- a/client/src/app/videos/+video-watch/video-duration-formatter.pipe.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { I18n } from '@ngx-translate/i18n-polyfill' - -@Pipe({ - name: 'myVideoDurationFormatter' -}) -export class VideoDurationPipe implements PipeTransform { - - constructor (private i18n: I18n) { - - } - - transform (value: number): string { - const hours = Math.floor(value / 3600) - const minutes = Math.floor((value % 3600) / 60) - const seconds = value % 60 - - if (hours > 0) { - return this.i18n('{{hours}} h {{minutes}} min {{seconds}} sec', { hours, minutes, seconds }) - } - - if (minutes > 0) { - return this.i18n('{{minutes}} min {{seconds}} sec', { minutes, seconds }) - } - - return this.i18n('{{seconds}} sec', { seconds }) - } -} diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.html b/client/src/app/videos/+video-watch/video-watch-playlist.component.html deleted file mode 100644 index 246ef83cf..000000000 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.html +++ /dev/null @@ -1,46 +0,0 @@ -
-
-
- {{ playlist.displayName }} - - Unlisted - Private - Public -
- -
-
{{ playlist.ownerBy }}
-
- {{ currentPlaylistPosition }}{{ playlistPagination.totalItems }} -
-
- -
- - - -
-
- -
- -
-
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss deleted file mode 100644 index 0b0a2a899..000000000 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss +++ /dev/null @@ -1,83 +0,0 @@ -@import '_variables'; -@import '_mixins'; -@import '_bootstrap-variables'; -@import '_miniature'; - -.playlist { - min-width: 200px; - max-width: 470px; - height: 66vh; - background-color: pvar(--mainBackgroundColor); - overflow-y: auto; - border-bottom: 1px solid $separator-border-color; - - .playlist-info { - padding: 5px 30px; - background-color: #e4e4e4; - - .playlist-display-name { - font-size: 18px; - font-weight: $font-semibold; - margin-bottom: 5px; - } - - .playlist-by-index { - color: pvar(--greyForegroundColor); - display: flex; - - .playlist-by { - margin-right: 5px; - } - - .playlist-index span:first-child::after { - content: '/'; - margin: 0 3px; - } - } - - .playlist-controls { - display: flex; - margin: 10px 0; - - my-global-icon:not(:last-child) { - margin-right: .5rem; - } - - my-global-icon { - &:not(.active) { - opacity: .5 - } - - ::ng-deep { - cursor: pointer; - } - } - } - } - - my-video-playlist-element-miniature { - ::ng-deep { - .video { - .position { - margin-right: 0; - } - - .video-info { - .video-info-name { - font-size: 15px; - } - } - } - - my-video-thumbnail { - @include thumbnail-size-component(90px, 50px); - } - - .fake-thumbnail { - width: 90px; - height: 50px; - } - } - } -} - diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts deleted file mode 100644 index 2c21be643..000000000 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Component, Input } from '@angular/core' -import { Router } from '@angular/router' -import { AuthService, ComponentPagination, LocalStorageService, Notifier, SessionStorageService, UserService } from '@app/core' -import { peertubeLocalStorage, peertubeSessionStorage } from '@app/helpers/peertube-web-storage' -import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' - -@Component({ - selector: 'my-video-watch-playlist', - templateUrl: './video-watch-playlist.component.html', - styleUrls: [ './video-watch-playlist.component.scss' ] -}) -export class VideoWatchPlaylistComponent { - static LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'auto_play_video_playlist' - static SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'loop_playlist' - - @Input() video: VideoDetails - @Input() playlist: VideoPlaylist - - playlistElements: VideoPlaylistElement[] = [] - playlistPagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 30, - totalItems: null - } - - autoPlayNextVideoPlaylist: boolean - autoPlayNextVideoPlaylistSwitchText = '' - loopPlaylist: boolean - loopPlaylistSwitchText = '' - noPlaylistVideos = false - currentPlaylistPosition = 1 - - constructor ( - private userService: UserService, - private auth: AuthService, - private notifier: Notifier, - private i18n: I18n, - private videoPlaylist: VideoPlaylistService, - private localStorageService: LocalStorageService, - private sessionStorageService: SessionStorageService, - private router: Router - ) { - // defaults to true - this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn() - ? this.auth.getUser().autoPlayNextVideoPlaylist - : this.localStorageService.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false' - this.setAutoPlayNextVideoPlaylistSwitchText() - - // defaults to false - this.loopPlaylist = this.sessionStorageService.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true' - this.setLoopPlaylistSwitchText() - } - - onPlaylistVideosNearOfBottom () { - // Last page - if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return - - this.playlistPagination.currentPage += 1 - this.loadPlaylistElements(this.playlist,false) - } - - onElementRemoved (playlistElement: VideoPlaylistElement) { - this.playlistElements = this.playlistElements.filter(e => e.id !== playlistElement.id) - - this.playlistPagination.totalItems-- - } - - isPlaylistOwned () { - return this.playlist.isLocal === true && - this.auth.isLoggedIn() && - this.playlist.ownerAccount.name === this.auth.getUser().username - } - - isUnlistedPlaylist () { - return this.playlist.privacy.id === VideoPlaylistPrivacy.UNLISTED - } - - isPrivatePlaylist () { - return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE - } - - isPublicPlaylist () { - return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC - } - - loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { - this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination) - .subscribe(({ total, data }) => { - this.playlistElements = this.playlistElements.concat(data) - this.playlistPagination.totalItems = total - - const firstAvailableVideos = this.playlistElements.find(e => !!e.video) - if (!firstAvailableVideos) { - this.noPlaylistVideos = true - return - } - - this.updatePlaylistIndex(this.video) - - if (redirectToFirst) { - const extras = { - queryParams: { - start: firstAvailableVideos.startTimestamp, - stop: firstAvailableVideos.stopTimestamp, - videoId: firstAvailableVideos.video.uuid - }, - replaceUrl: true - } - this.router.navigate([], extras) - } - }) - } - - updatePlaylistIndex (video: VideoDetails) { - if (this.playlistElements.length === 0 || !video) return - - for (const playlistElement of this.playlistElements) { - if (playlistElement.video && playlistElement.video.id === video.id) { - this.currentPlaylistPosition = playlistElement.position - return - } - } - - // Load more videos to find our video - this.onPlaylistVideosNearOfBottom() - } - - findNextPlaylistVideo (position = this.currentPlaylistPosition): VideoPlaylistElement { - if (this.currentPlaylistPosition >= this.playlistPagination.totalItems) { - // we have reached the end of the playlist: either loop or stop - if (this.loopPlaylist) { - this.currentPlaylistPosition = position = 0 - } else { - return - } - } - - const next = this.playlistElements.find(e => e.position === position) - - if (!next || !next.video) { - return this.findNextPlaylistVideo(position + 1) - } - - return next - } - - navigateToNextPlaylistVideo () { - const next = this.findNextPlaylistVideo(this.currentPlaylistPosition + 1) - if (!next) return - const start = next.startTimestamp - const stop = next.stopTimestamp - this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } }) - } - - switchAutoPlayNextVideoPlaylist () { - this.autoPlayNextVideoPlaylist = !this.autoPlayNextVideoPlaylist - this.setAutoPlayNextVideoPlaylistSwitchText() - - peertubeLocalStorage.setItem( - VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST, - this.autoPlayNextVideoPlaylist.toString() - ) - - if (this.auth.isLoggedIn()) { - const details = { - autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist - } - - this.userService.updateMyProfile(details).subscribe( - () => { - this.auth.refreshUserInformation() - }, - err => this.notifier.error(err.message) - ) - } - } - - switchLoopPlaylist () { - this.loopPlaylist = !this.loopPlaylist - this.setLoopPlaylistSwitchText() - - peertubeSessionStorage.setItem( - VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST, - this.loopPlaylist.toString() - ) - } - - private setAutoPlayNextVideoPlaylistSwitchText () { - this.autoPlayNextVideoPlaylistSwitchText = this.autoPlayNextVideoPlaylist - ? this.i18n('Stop autoplaying next video') - : this.i18n('Autoplay next video') - } - - private setLoopPlaylistSwitchText () { - this.loopPlaylistSwitchText = this.loopPlaylist - ? this.i18n('Stop looping playlist videos') - : this.i18n('Loop playlist videos') - } -} diff --git a/client/src/app/videos/+video-watch/video-watch-routing.module.ts b/client/src/app/videos/+video-watch/video-watch-routing.module.ts deleted file mode 100644 index d8fecb87d..000000000 --- a/client/src/app/videos/+video-watch/video-watch-routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' -import { VideoWatchComponent } from './video-watch.component' - -const videoWatchRoutes: Routes = [ - { - path: 'playlist/:playlistId', - component: VideoWatchComponent, - canActivate: [ MetaGuard ] - }, - { - path: ':videoId/comments/:commentId', - redirectTo: ':videoId' - }, - { - path: ':videoId', - component: VideoWatchComponent, - canActivate: [ MetaGuard ] - } -] - -@NgModule({ - imports: [ RouterModule.forChild(videoWatchRoutes) ], - exports: [ RouterModule ] -}) -export class VideoWatchRoutingModule {} diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html deleted file mode 100644 index 0447268f0..000000000 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ /dev/null @@ -1,277 +0,0 @@ -
- -
-
- Sorry, but this video is not available because the remote instance is not responding. -
- Please try again later. -
- -
- - -
- -
-
- The video is being imported, it will be available when the import is finished. -
- -
- The video is being transcoded, it may not work properly. -
- -
- This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}. -
- -
-
This video is blocked.
- {{ video.blockedReason }} -
-
- - -
-
-
-
-
-

{{ video.name }}

- -
- Published • {{ video.views | myNumberFormatter }} views -
-
- -
-
-

{{ video.name }}

-
- -
-
- Published • {{ video.views | myNumberFormatter }} views -
- -
-
- - - - - - - - -
- - -
- -
-
- - -
- -
-
-
- -
-
-
-
- -
- -
-
-
- - - -
- -
- -
-
- -
- Show more - - -
- -
- Show less - -
-
- -
-
- Privacy - {{ video.privacy.label }} -
- -
- Origin instance - {{ video.originInstanceHost }} -
- -
- Originally published - {{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }} -
- -
- Category - {{ video.category.label }} - {{ video.category.label }} -
- -
- Licence - {{ video.licence.label }} - {{ video.licence.label }} -
- -
- Language - {{ video.language.label }} - {{ video.language.label }} -
- -
- Tags - {{ tag }} -
- -
- Duration - {{ video.duration | myVideoDurationFormatter }} -
-
- - -
- - -
- -
-
- - Friendly Reminder: - - the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers. - - - More information -
- -
- OK -
-
-
- - - - - diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss deleted file mode 100644 index 2e083982e..000000000 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ /dev/null @@ -1,607 +0,0 @@ -@import '_variables'; -@import '_mixins'; -@import '_bootstrap-variables'; -@import '_miniature'; - -$player-factor: 1.7; // 16/9 -$video-info-margin-left: 44px; - -@function getPlayerHeight($width){ - @return calc(#{$width} / #{$player-factor}) -} - -@function getPlayerWidth($height){ - @return calc(#{$height} * #{$player-factor}) -} - -@mixin playlist-below-player { - width: 100% !important; - height: auto !important; - max-height: 300px !important; - max-width: initial; - border-bottom: 1px solid $separator-border-color !important; -} - -.root { - &.theater-enabled #video-wrapper { - flex-direction: column; - justify-content: center; - - #videojs-wrapper { - width: 100%; - } - - ::ng-deep .video-js { - $height: calc(100vh - #{$header-height} - #{$theater-bottom-space}); - - height: $height; - width: 100%; - max-width: initial; - } - - my-video-watch-playlist ::ng-deep .playlist { - @include playlist-below-player; - } - } -} - -.blocked-label { - font-weight: $font-semibold; -} - -#video-wrapper { - background-color: #000; - display: flex; - justify-content: center; - - #videojs-wrapper { - display: flex; - justify-content: center; - flex-grow: 1; - } - - .remote-server-down { - color: #fff; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - justify-content: center; - background-color: #141313; - width: 100%; - font-size: 24px; - height: 500px; - - @media screen and (max-width: 1000px) { - font-size: 20px; - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } - } - - ::ng-deep .video-js { - width: 100%; - max-width: getPlayerWidth(66vh); - height: 66vh; - - // VideoJS create an inner video player - video { - outline: 0; - position: relative !important; - } - } - - @media screen and (max-width: 600px) { - .remote-server-down, - ::ng-deep .video-js { - width: 100vw; - height: getPlayerHeight(100vw) - } - } -} - -.alert { - text-align: center; - border-radius: 0; -} - -.flex-direction-column { - flex-direction: column; -} - -#video-not-found { - height: 300px; - line-height: 300px; - margin-top: 50px; - text-align: center; - font-weight: $font-semibold; - font-size: 15px; -} - -.video-bottom { - display: flex; - margin-top: 1.5rem; - - .video-info { - flex-grow: 1; - // Set min width for flex item - min-width: 1px; - max-width: 100%; - - .video-info-first-row { - display: flex; - - & > div:first-child { - flex-grow: 1; - } - - .video-info-name { - margin-right: 30px; - min-height: 40px; // Align with the action buttons - font-size: 27px; - font-weight: $font-semibold; - flex-grow: 1; - } - - .video-info-first-row-bottom { - display: flex; - flex-wrap: wrap; - align-items: center; - width: 100%; - } - - .video-info-date-views { - align-self: start; - margin-bottom: 10px; - margin-right: 10px; - font-size: 1em; - } - - .video-info-channel { - font-weight: $font-semibold; - font-size: 15px; - - a { - @include disable-default-a-behaviour; - - color: pvar(--mainForegroundColor); - - &:hover { - opacity: 0.8; - } - - img { - @include avatar(18px); - - margin: -2px 5px 0 0; - } - } - - .video-info-channel-left { - flex-grow: 1; - - .video-info-channel-left-links { - display: flex; - flex-direction: column; - position: relative; - line-height: 1.37; - - a:nth-of-type(2) { - font-weight: 500; - font-size: 90%; - } - } - } - - my-subscribe-button { - margin-left: 5px; - } - } - - my-feed { - margin-left: 5px; - margin-top: 1px; - } - - .video-actions-rates { - margin: 0 0 10px 0; - align-items: start; - width: max-content; - margin-left: auto; - - .video-actions { - height: 40px; // Align with the title - display: flex; - align-items: center; - - .action-button:not(:first-child), - .action-dropdown, - my-video-actions-dropdown { - margin-left: 5px; - } - - ::ng-deep.action-button { - @include peertube-button; - @include button-with-icon(21px, 0, -1px); - @include apply-svg-color(pvar(--actionButtonColor)); - - font-size: 100%; - font-weight: $font-semibold; - display: inline-block; - padding: 0 10px 0 10px; - white-space: nowrap; - background-color: transparent !important; - color: pvar(--actionButtonColor); - text-transform: uppercase; - - &::after { - display: none; - } - - &:hover { - opacity: 0.9; - } - - &.action-button-like, - &.action-button-dislike { - filter: brightness(120%); - - .count { - margin-right: 5px; - } - } - - &.action-button-like.activated { - .count { - color: pvar(--activatedActionButtonColor); - } - - my-global-icon { - @include apply-svg-color(pvar(--activatedActionButtonColor)); - } - } - - &.action-button-dislike.activated { - .count { - color: pvar(--activatedActionButtonColor); - } - - my-global-icon { - @include apply-svg-color(pvar(--activatedActionButtonColor)); - } - } - - &.action-button-support { - color: pvar(--supportButtonColor); - - my-global-icon { - @include apply-svg-color(pvar(--supportButtonColor)); - } - } - - &.action-button-support { - my-global-icon { - ::ng-deep path:first-child { - fill: pvar(--supportButtonHeartColor) !important; - } - } - } - - &.action-button-save { - my-global-icon { - top: 0 !important; - right: -1px; - } - } - - .icon-text { - margin-left: 3px; - } - } - } - - .video-info-likes-dislikes-bar-outer-container { - position: relative; - } - - .video-info-likes-dislikes-bar-inner-container { - position: absolute; - height: 20px; - } - - .video-info-likes-dislikes-bar { - $likes-bar-height: 2px; - height: $likes-bar-height; - margin-top: -$likes-bar-height; - width: 120px; - background-color: #ccc; - position: relative; - top: 10px; - - .likes-bar { - height: 100%; - background-color: #909090; - - &.liked { - background-color: pvar(--activatedActionButtonColor); - } - } - } - } - } - - .video-info-description { - margin: 20px 0; - margin-left: $video-info-margin-left; - font-size: 15px; - - .video-info-description-html { - @include peertube-word-wrap; - - /deep/ a { - text-decoration: none; - } - } - - .glyphicon, .description-loading { - margin-left: 3px; - } - - .description-loading { - display: inline-block; - } - - .video-info-description-more { - cursor: pointer; - font-weight: $font-semibold; - color: pvar(--greyForegroundColor); - font-size: 14px; - - .glyphicon { - position: relative; - top: 2px; - } - } - } - - .video-attributes { - margin-left: $video-info-margin-left; - } - - .video-attributes .video-attribute { - font-size: 13px; - display: block; - margin-bottom: 12px; - - .video-attribute-label { - min-width: 142px; - padding-right: 5px; - display: inline-block; - color: pvar(--greyForegroundColor); - font-weight: $font-bold; - } - - a.video-attribute-value { - @include disable-default-a-behaviour; - color: pvar(--mainForegroundColor); - - &:hover { - opacity: 0.9; - } - } - - &.video-attribute-tags { - .video-attribute-value:not(:nth-child(2)) { - &::before { - content: ', ' - } - } - } - } - } - - ::ng-deep .other-videos { - padding-left: 15px; - min-width: $video-miniature-width; - - @media screen and (min-width: 1800px - (3* $video-miniature-width)) { - width: min-content; - } - - .title-page { - margin: 0 !important; - } - - .video-miniature { - display: flex; - width: max-content; - height: 100%; - padding-bottom: 20px; - flex-wrap: wrap; - } - - .video-bottom { - @media screen and (max-width: 1800px - (3* $video-miniature-width)) { - margin-left: 1rem; - } - @media screen and (max-width: 500px) { - margin-left: 0; - margin-top: .5rem; - } - } - } -} - -my-video-comments { - display: inline-block; - width: 100%; - margin-bottom: 20px; -} - -// If the view is not expanded, take into account the menu -.privacy-concerns { - z-index: z(dropdown) + 1; - width: calc(100% - #{$menu-width}); -} - -@media screen and (max-width: $small-view) { - .privacy-concerns { - margin-left: $menu-width - 15px; // Menu is absolute - } -} - -:host-context(.expanded) { - .privacy-concerns { - width: 100%; - margin-left: -15px; - } -} - -.privacy-concerns { - position: fixed; - bottom: 0; - z-index: z(privacymsg); - - padding: 5px 15px; - - display: flex; - flex-wrap: nowrap; - align-items: center; - justify-content: space-between; - background-color: rgba(0, 0, 0, 0.9); - color: #fff; - - .privacy-concerns-text { - margin: 0 5px; - } - - a { - @include disable-default-a-behaviour; - - color: pvar(--mainColor); - transition: color 0.3s; - - &:hover { - color: #fff; - } - } - - .privacy-concerns-button { - padding: 5px 8px 5px 7px; - margin-left: auto; - border-radius: 3px; - white-space: nowrap; - cursor: pointer; - transition: background-color 0.3s; - font-weight: $font-semibold; - - &:hover { - background-color: #000; - } - } - - .privacy-concerns-okay { - background-color: pvar(--mainColor); - margin-left: 10px; - } -} - -@media screen and (max-width: 1600px) { - .video-bottom .video-info .video-attributes .video-attribute { - margin-bottom: 5px; - } -} - -@media screen and (max-width: 1300px) { - .privacy-concerns { - font-size: 12px; - padding: 2px 5px; - - .privacy-concerns-text { - margin: 0; - } - } -} - -@media screen and (max-width: 1100px) { - #video-wrapper { - flex-direction: column; - justify-content: center; - - my-video-watch-playlist ::ng-deep .playlist { - @include playlist-below-player; - } - } - - .video-bottom { - flex-direction: column; - - ::ng-deep .other-videos { - padding-left: 0 !important; - - ::ng-deep .video-miniature { - flex-direction: row; - width: auto; - } - } - } -} - -@media screen and (max-width: 600px) { - .video-bottom { - margin-top: 20px !important; - padding-bottom: 20px !important; - - .video-info { - padding: 0; - - .video-info-first-row { - - .video-info-name { - font-size: 20px; - height: auto; - } - } - } - } - - ::ng-deep .other-videos .video-miniature { - flex-direction: column; - } - - .privacy-concerns { - width: 100%; - - strong { - display: none; - } - } -} - -@media screen and (max-width: 450px) { - .video-bottom { - .action-button .icon-text { - display: none !important; - } - - .video-info .video-info-first-row { - .video-info-name { - font-size: 18px; - } - - .video-info-date-views { - font-size: 14px; - } - - .video-actions-rates { - margin-top: 10px; - } - } - - .video-info-description { - font-size: 14px !important; - } - } -} diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts deleted file mode 100644 index 5b0b34c80..000000000 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ /dev/null @@ -1,782 +0,0 @@ -import { Hotkey, HotkeysService } from 'angular2-hotkeys' -import { forkJoin, Observable, Subscription } from 'rxjs' -import { catchError } from 'rxjs/operators' -import { PlatformLocation } from '@angular/common' -import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, AuthUser, ConfirmService, MarkdownService, Notifier, RestExtractor, ServerService, UserService } from '@app/core' -import { HooksService } from '@app/core/plugins/hooks.service' -import { RedirectService } from '@app/core/routing/redirect.service' -import { isXPercentInViewport, peertubeLocalStorage, scrollToTop } from '@app/helpers' -import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' -import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' -import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { MetaService } from '@ngx-meta/core' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { ServerConfig, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' -import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' -import { - CustomizationOptions, - P2PMediaLoaderOptions, - PeertubePlayerManager, - PeertubePlayerManagerOptions, - PlayerMode, - videojs -} from '../../../assets/player/peertube-player-manager' -import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' -import { environment } from '../../../environments/environment' -import { VideoShareComponent } from './modal/video-share.component' -import { VideoSupportComponent } from './modal/video-support.component' -import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' - -@Component({ - selector: 'my-video-watch', - templateUrl: './video-watch.component.html', - styleUrls: [ './video-watch.component.scss' ] -}) -export class VideoWatchComponent implements OnInit, OnDestroy { - private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' - - @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent - @ViewChild('videoShareModal') videoShareModal: VideoShareComponent - @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent - @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent - - player: any - playerElement: HTMLVideoElement - theaterEnabled = false - userRating: UserVideoRateType = null - descriptionLoading = false - - video: VideoDetails = null - videoCaptions: VideoCaption[] = [] - - playlist: VideoPlaylist = null - - completeDescriptionShown = false - completeVideoDescription: string - shortVideoDescription: string - videoHTMLDescription = '' - likesBarTooltipText = '' - hasAlreadyAcceptedPrivacyConcern = false - remoteServerDown = false - hotkeys: Hotkey[] = [] - - tooltipLike = '' - tooltipDislike = '' - tooltipSupport = '' - tooltipSaveToPlaylist = '' - - private nextVideoUuid = '' - private nextVideoTitle = '' - private currentTime: number - private paramsSub: Subscription - private queryParamsSub: Subscription - private configSub: Subscription - - private serverConfig: ServerConfig - - constructor ( - private elementRef: ElementRef, - private changeDetector: ChangeDetectorRef, - private route: ActivatedRoute, - private router: Router, - private videoService: VideoService, - private playlistService: VideoPlaylistService, - private confirmService: ConfirmService, - private metaService: MetaService, - private authService: AuthService, - private userService: UserService, - private serverService: ServerService, - private restExtractor: RestExtractor, - private notifier: Notifier, - private markdownService: MarkdownService, - private zone: NgZone, - private redirectService: RedirectService, - private videoCaptionService: VideoCaptionService, - private i18n: I18n, - private hotkeysService: HotkeysService, - private hooks: HooksService, - private location: PlatformLocation, - @Inject(LOCALE_ID) private localeId: string - ) { - this.tooltipLike = this.i18n('Like this video') - this.tooltipDislike = this.i18n('Dislike this video') - this.tooltipSupport = this.i18n('Support options for this video') - this.tooltipSaveToPlaylist = this.i18n('Save to playlist') - } - - get user () { - return this.authService.getUser() - } - - get anonymousUser () { - return this.userService.getAnonymousUser() - } - - async ngOnInit () { - this.serverConfig = this.serverService.getTmpConfig() - - this.configSub = this.serverService.getConfig() - .subscribe(config => { - this.serverConfig = config - - if ( - isWebRTCDisabled() || - this.serverConfig.tracker.enabled === false || - getStoredP2PEnabled() === false || - peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' - ) { - this.hasAlreadyAcceptedPrivacyConcern = true - } - }) - - this.paramsSub = this.route.params.subscribe(routeParams => { - const videoId = routeParams[ 'videoId' ] - if (videoId) this.loadVideo(videoId) - - const playlistId = routeParams[ 'playlistId' ] - if (playlistId) this.loadPlaylist(playlistId) - }) - - this.queryParamsSub = this.route.queryParams.subscribe(async queryParams => { - const videoId = queryParams[ 'videoId' ] - if (videoId) this.loadVideo(videoId) - - const start = queryParams[ 'start' ] - if (this.player && start) this.player.currentTime(parseInt(start, 10)) - }) - - this.initHotkeys() - - this.theaterEnabled = getStoredTheater() - - this.hooks.runAction('action:video-watch.init', 'video-watch') - } - - ngOnDestroy () { - this.flushPlayer() - - // Unsubscribe subscriptions - if (this.paramsSub) this.paramsSub.unsubscribe() - if (this.queryParamsSub) this.queryParamsSub.unsubscribe() - - // Unbind hotkeys - this.hotkeysService.remove(this.hotkeys) - } - - setLike () { - if (this.isUserLoggedIn() === false) return - - // Already liked this video - if (this.userRating === 'like') this.setRating('none') - else this.setRating('like') - } - - setDislike () { - if (this.isUserLoggedIn() === false) return - - // Already disliked this video - if (this.userRating === 'dislike') this.setRating('none') - else this.setRating('dislike') - } - - getRatePopoverText () { - if (this.isUserLoggedIn()) return undefined - - return this.i18n('You need to be connected to rate this content.') - } - - showMoreDescription () { - if (this.completeVideoDescription === undefined) { - return this.loadCompleteDescription() - } - - this.updateVideoDescription(this.completeVideoDescription) - this.completeDescriptionShown = true - } - - showLessDescription () { - this.updateVideoDescription(this.shortVideoDescription) - this.completeDescriptionShown = false - } - - loadCompleteDescription () { - this.descriptionLoading = true - - this.videoService.loadCompleteDescription(this.video.descriptionPath) - .subscribe( - description => { - this.completeDescriptionShown = true - this.descriptionLoading = false - - this.shortVideoDescription = this.video.description - this.completeVideoDescription = description - - this.updateVideoDescription(this.completeVideoDescription) - }, - - error => { - this.descriptionLoading = false - this.notifier.error(error.message) - } - ) - } - - showSupportModal () { - this.pausePlayer() - - this.videoSupportModal.show() - } - - showShareModal () { - this.pausePlayer() - - this.videoShareModal.show(this.currentTime) - } - - isUserLoggedIn () { - return this.authService.isLoggedIn() - } - - getVideoTags () { - if (!this.video || Array.isArray(this.video.tags) === false) return [] - - return this.video.tags - } - - onRecommendations (videos: Video[]) { - if (videos.length > 0) { - // The recommended videos's first element should be the next video - const video = videos[0] - this.nextVideoUuid = video.uuid - this.nextVideoTitle = video.name - } - } - - onModalOpened () { - this.pausePlayer() - } - - onVideoRemoved () { - this.redirectService.redirectToHomepage() - } - - declinedPrivacyConcern () { - peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'false') - this.hasAlreadyAcceptedPrivacyConcern = false - } - - acceptedPrivacyConcern () { - peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true') - this.hasAlreadyAcceptedPrivacyConcern = true - } - - isVideoToTranscode () { - return this.video && this.video.state.id === VideoState.TO_TRANSCODE - } - - isVideoToImport () { - return this.video && this.video.state.id === VideoState.TO_IMPORT - } - - hasVideoScheduledPublication () { - return this.video && this.video.scheduledUpdate !== undefined - } - - isVideoBlur (video: Video) { - return video.isVideoNSFWForUser(this.user, this.serverConfig) - } - - isAutoPlayEnabled () { - return ( - (this.user && this.user.autoPlayNextVideo) || - this.anonymousUser.autoPlayNextVideo - ) - } - - handleTimestampClicked (timestamp: number) { - if (this.player) this.player.currentTime(timestamp) - scrollToTop() - } - - isPlaylistAutoPlayEnabled () { - return ( - (this.user && this.user.autoPlayNextVideoPlaylist) || - this.anonymousUser.autoPlayNextVideoPlaylist - ) - } - - private loadVideo (videoId: string) { - // Video did not change - if (this.video && this.video.uuid === videoId) return - - if (this.player) this.player.pause() - - const videoObs = this.hooks.wrapObsFun( - this.videoService.getVideo.bind(this.videoService), - { videoId }, - 'video-watch', - 'filter:api.video-watch.video.get.params', - 'filter:api.video-watch.video.get.result' - ) - - // Video did change - forkJoin([ - videoObs, - this.videoCaptionService.listCaptions(videoId) - ]) - .pipe( - // If 401, the video is private or blocked so redirect to 404 - catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])) - ) - .subscribe(([ video, captionsResult ]) => { - const queryParams = this.route.snapshot.queryParams - - const urlOptions = { - startTime: queryParams.start, - stopTime: queryParams.stop, - - muted: queryParams.muted, - loop: queryParams.loop, - subtitle: queryParams.subtitle, - - playerMode: queryParams.mode, - peertubeLink: false - } - - this.onVideoFetched(video, captionsResult.data, urlOptions) - .catch(err => this.handleError(err)) - }) - } - - private loadPlaylist (playlistId: string) { - // Playlist did not change - if (this.playlist && this.playlist.uuid === playlistId) return - - this.playlistService.getVideoPlaylist(playlistId) - .pipe( - // If 401, the video is private or blocked so redirect to 404 - catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])) - ) - .subscribe(playlist => { - this.playlist = playlist - - const videoId = this.route.snapshot.queryParams['videoId'] - this.videoWatchPlaylist.loadPlaylistElements(playlist, !videoId) - }) - } - - private updateVideoDescription (description: string) { - this.video.description = description - this.setVideoDescriptionHTML() - .catch(err => console.error(err)) - } - - private async setVideoDescriptionHTML () { - const html = await this.markdownService.textMarkdownToHTML(this.video.description) - this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html) - } - - private setVideoLikesBarTooltipText () { - this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', { - likesNumber: this.video.likes, - dislikesNumber: this.video.dislikes - }) - } - - private handleError (err: any) { - const errorMessage: string = typeof err === 'string' ? err : err.message - if (!errorMessage) return - - // Display a message in the video player instead of a notification - if (errorMessage.indexOf('from xs param') !== -1) { - this.flushPlayer() - this.remoteServerDown = true - this.changeDetector.detectChanges() - - return - } - - this.notifier.error(errorMessage) - } - - private checkUserRating () { - // Unlogged users do not have ratings - if (this.isUserLoggedIn() === false) return - - this.videoService.getUserVideoRating(this.video.id) - .subscribe( - ratingObject => { - if (ratingObject) { - this.userRating = ratingObject.rating - } - }, - - err => this.notifier.error(err.message) - ) - } - - private async onVideoFetched ( - video: VideoDetails, - videoCaptions: VideoCaption[], - urlOptions: CustomizationOptions & { playerMode: PlayerMode } - ) { - this.video = video - this.videoCaptions = videoCaptions - - // Re init attributes - this.descriptionLoading = false - this.completeDescriptionShown = false - this.remoteServerDown = false - this.currentTime = undefined - - this.videoWatchPlaylist.updatePlaylistIndex(video) - - if (this.isVideoBlur(this.video)) { - const res = await this.confirmService.confirm( - this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'), - this.i18n('Mature or explicit content') - ) - if (res === false) return this.location.back() - } - - // Flush old player if needed - this.flushPlayer() - - // Build video element, because videojs removes it on dispose - const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper') - this.playerElement = document.createElement('video') - this.playerElement.className = 'video-js vjs-peertube-skin' - this.playerElement.setAttribute('playsinline', 'true') - playerElementWrapper.appendChild(this.playerElement) - - const params = { - video: this.video, - videoCaptions, - urlOptions, - user: this.user - } - const { playerMode, playerOptions } = await this.hooks.wrapFun( - this.buildPlayerManagerOptions.bind(this), - params, - 'video-watch', - 'filter:internal.video-watch.player.build-options.params', - 'filter:internal.video-watch.player.build-options.result' - ) - - this.zone.runOutsideAngular(async () => { - this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player) - this.player.focus() - - this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) - - this.player.on('timeupdate', () => { - this.currentTime = Math.floor(this.player.currentTime()) - }) - - /** - * replaces this.player.one('ended') - * 'condition()': true to make the upnext functionality trigger, - * false to disable the upnext functionality - * go to the next video in 'condition()' if you don't want of the timer. - * 'next': function triggered at the end of the timer. - * 'suspended': function used at each clic of the timer checking if we need - * to reset progress and wait until 'suspended' becomes truthy again. - */ - this.player.upnext({ - timeout: 10000, // 10s - headText: this.i18n('Up Next'), - cancelText: this.i18n('Cancel'), - suspendedText: this.i18n('Autoplay is suspended'), - getTitle: () => this.nextVideoTitle, - next: () => this.zone.run(() => this.autoplayNext()), - condition: () => { - if (this.playlist) { - if (this.isPlaylistAutoPlayEnabled()) { - // upnext will not trigger, and instead the next video will play immediately - this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) - } - } else if (this.isAutoPlayEnabled()) { - return true // upnext will trigger - } - return false // upnext will not trigger, and instead leave the video stopping - }, - suspended: () => { - return ( - !isXPercentInViewport(this.player.el(), 80) || - !document.getElementById('content').contains(document.activeElement) - ) - } - }) - - this.player.one('stopped', () => { - if (this.playlist) { - if (this.isPlaylistAutoPlayEnabled()) this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) - } - }) - - this.player.on('theaterChange', (_: any, enabled: boolean) => { - this.zone.run(() => this.theaterEnabled = enabled) - }) - - this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { player: this.player }) - }) - - this.setVideoDescriptionHTML() - this.setVideoLikesBarTooltipText() - - this.setOpenGraphTags() - this.checkUserRating() - - this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', { videojs }) - } - - private autoplayNext () { - if (this.playlist) { - this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) - } else if (this.nextVideoUuid) { - this.router.navigate([ '/videos/watch', this.nextVideoUuid ]) - } - } - - private setRating (nextRating: UserVideoRateType) { - const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable } = { - like: this.videoService.setVideoLike, - dislike: this.videoService.setVideoDislike, - none: this.videoService.unsetVideoLike - } - - ratingMethods[nextRating].call(this.videoService, this.video.id) - .subscribe( - () => { - // Update the video like attribute - this.updateVideoRating(this.userRating, nextRating) - this.userRating = nextRating - }, - - (err: { message: string }) => this.notifier.error(err.message) - ) - } - - private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) { - let likesToIncrement = 0 - let dislikesToIncrement = 0 - - if (oldRating) { - if (oldRating === 'like') likesToIncrement-- - if (oldRating === 'dislike') dislikesToIncrement-- - } - - if (newRating === 'like') likesToIncrement++ - if (newRating === 'dislike') dislikesToIncrement++ - - this.video.likes += likesToIncrement - this.video.dislikes += dislikesToIncrement - - this.video.buildLikeAndDislikePercents() - this.setVideoLikesBarTooltipText() - } - - private setOpenGraphTags () { - this.metaService.setTitle(this.video.name) - - this.metaService.setTag('og:type', 'video') - - this.metaService.setTag('og:title', this.video.name) - this.metaService.setTag('name', this.video.name) - - this.metaService.setTag('og:description', this.video.description) - this.metaService.setTag('description', this.video.description) - - this.metaService.setTag('og:image', this.video.previewPath) - - this.metaService.setTag('og:duration', this.video.duration.toString()) - - this.metaService.setTag('og:site_name', 'PeerTube') - - this.metaService.setTag('og:url', window.location.href) - this.metaService.setTag('url', window.location.href) - } - - private isAutoplay () { - // We'll jump to the thread id, so do not play the video - if (this.route.snapshot.params['threadId']) return false - - // Otherwise true by default - if (!this.user) return true - - // Be sure the autoPlay is set to false - return this.user.autoPlayVideo !== false - } - - private flushPlayer () { - // Remove player if it exists - if (this.player) { - try { - this.player.dispose() - this.player = undefined - } catch (err) { - console.error('Cannot dispose player.', err) - } - } - } - - private buildPlayerManagerOptions (params: { - video: VideoDetails, - videoCaptions: VideoCaption[], - urlOptions: CustomizationOptions & { playerMode: PlayerMode }, - user?: AuthUser - }) { - const { video, videoCaptions, urlOptions, user } = params - const getStartTime = () => { - const byUrl = urlOptions.startTime !== undefined - const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) - - if (byUrl) { - return timeToInt(urlOptions.startTime) - } else if (byHistory) { - return video.userHistory.currentTime - } else { - return 0 - } - } - - let startTime = getStartTime() - // If we are at the end of the video, reset the timer - if (video.duration - startTime <= 1) startTime = 0 - - const playerCaptions = videoCaptions.map(c => ({ - label: c.language.label, - language: c.language.id, - src: environment.apiUrl + c.captionPath - })) - - const options: PeertubePlayerManagerOptions = { - common: { - autoplay: this.isAutoplay(), - nextVideo: () => this.zone.run(() => this.autoplayNext()), - - playerElement: this.playerElement, - onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, - - videoDuration: video.duration, - enableHotkeys: true, - inactivityTimeout: 2500, - poster: video.previewUrl, - - startTime, - stopTime: urlOptions.stopTime, - controls: urlOptions.controls, - muted: urlOptions.muted, - loop: urlOptions.loop, - subtitle: urlOptions.subtitle, - - peertubeLink: urlOptions.peertubeLink, - - theaterButton: true, - captions: videoCaptions.length !== 0, - - videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE - ? this.videoService.getVideoViewUrl(video.uuid) - : null, - embedUrl: video.embedUrl, - - language: this.localeId, - - userWatching: user && user.videosHistoryEnabled === true ? { - url: this.videoService.getUserWatchingVideoUrl(video.uuid), - authorizationHeader: this.authService.getRequestHeaderValue() - } : undefined, - - serverUrl: environment.apiUrl, - - videoCaptions: playerCaptions - }, - - webtorrent: { - videoFiles: video.files - } - } - - let mode: PlayerMode - - if (urlOptions.playerMode) { - if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' - else mode = 'webtorrent' - } else { - if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' - else mode = 'webtorrent' - } - - // p2p-media-loader needs TextEncoder, try to fallback on WebTorrent - if (typeof TextEncoder === 'undefined') { - mode = 'webtorrent' - } - - if (mode === 'p2p-media-loader') { - const hlsPlaylist = video.getHlsPlaylist() - - const p2pMediaLoader = { - playlistUrl: hlsPlaylist.playlistUrl, - segmentsSha256Url: hlsPlaylist.segmentsSha256Url, - redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), - trackerAnnounce: video.trackerUrls, - videoFiles: hlsPlaylist.files - } as P2PMediaLoaderOptions - - Object.assign(options, { p2pMediaLoader }) - } - - return { playerMode: mode, playerOptions: options } - } - - private pausePlayer () { - if (!this.player) return - - this.player.pause() - } - - private initHotkeys () { - this.hotkeys = [ - // These hotkeys are managed by the player - new Hotkey('f', e => e, undefined, this.i18n('Enter/exit fullscreen (requires player focus)')), - new Hotkey('space', e => e, undefined, this.i18n('Play/Pause the video (requires player focus)')), - new Hotkey('m', e => e, undefined, this.i18n('Mute/unmute the video (requires player focus)')), - - new Hotkey('0-9', e => e, undefined, this.i18n('Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)')), - - new Hotkey('up', e => e, undefined, this.i18n('Increase the volume (requires player focus)')), - new Hotkey('down', e => e, undefined, this.i18n('Decrease the volume (requires player focus)')), - - new Hotkey('right', e => e, undefined, this.i18n('Seek the video forward (requires player focus)')), - new Hotkey('left', e => e, undefined, this.i18n('Seek the video backward (requires player focus)')), - - new Hotkey('>', e => e, undefined, this.i18n('Increase playback rate (requires player focus)')), - new Hotkey('<', e => e, undefined, this.i18n('Decrease playback rate (requires player focus)')), - - new Hotkey('.', e => e, undefined, this.i18n('Navigate in the video frame by frame (requires player focus)')) - ] - - if (this.isUserLoggedIn()) { - this.hotkeys = this.hotkeys.concat([ - new Hotkey('shift+l', () => { - this.setLike() - return false - }, undefined, this.i18n('Like the video')), - - new Hotkey('shift+d', () => { - this.setDislike() - return false - }, undefined, this.i18n('Dislike the video')), - - new Hotkey('shift+s', () => { - this.subscribeButton.subscribed ? this.subscribeButton.unsubscribe() : this.subscribeButton.subscribe() - return false - }, undefined, this.i18n('Subscribe to the account')) - ]) - } - - this.hotkeysService.add(this.hotkeys) - } -} diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts deleted file mode 100644 index a1c54f065..000000000 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { QRCodeModule } from 'angularx-qrcode' -import { NgModule } from '@angular/core' -import { SharedFormModule } from '@app/shared/shared-forms' -import { SharedGlobalIconModule } from '@app/shared/shared-icons' -import { SharedMainModule } from '@app/shared/shared-main' -import { SharedModerationModule } from '@app/shared/shared-moderation' -import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' -import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' -import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' -import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' -import { VideoCommentAddComponent } from './comment/video-comment-add.component' -import { VideoCommentComponent } from './comment/video-comment.component' -import { VideoCommentService } from './comment/video-comment.service' -import { VideoCommentsComponent } from './comment/video-comments.component' -import { VideoShareComponent } from './modal/video-share.component' -import { VideoSupportComponent } from './modal/video-support.component' -import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' -import { VideoDurationPipe } from './video-duration-formatter.pipe' -import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' -import { VideoWatchRoutingModule } from './video-watch-routing.module' -import { VideoWatchComponent } from './video-watch.component' - -@NgModule({ - imports: [ - VideoWatchRoutingModule, - NgbTooltipModule, - QRCodeModule, - RecommendationsModule, - - SharedMainModule, - SharedFormModule, - SharedVideoMiniatureModule, - SharedVideoPlaylistModule, - SharedUserSubscriptionModule, - SharedModerationModule, - SharedGlobalIconModule - ], - - declarations: [ - VideoWatchComponent, - VideoWatchPlaylistComponent, - - VideoShareComponent, - VideoSupportComponent, - VideoCommentsComponent, - VideoCommentAddComponent, - VideoCommentComponent, - - TimestampRouteTransformerDirective, - VideoDurationPipe, - TimestampRouteTransformerDirective - ], - - exports: [ - VideoWatchComponent, - - TimestampRouteTransformerDirective - ], - - providers: [ - VideoCommentService - ] -}) -export class VideoWatchModule { } -- cgit v1.2.3