From 67ed6552b831df66713bac9e672738796128d33f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:10:17 +0200 Subject: Reorganize client shared modules --- .../src/app/shared/video/abstract-video-list.html | 49 --- .../src/app/shared/video/abstract-video-list.scss | 75 ---- client/src/app/shared/video/abstract-video-list.ts | 308 ---------------- client/src/app/shared/video/feed.component.html | 15 - client/src/app/shared/video/feed.component.scss | 20 - client/src/app/shared/video/feed.component.ts | 11 - .../shared/video/infinite-scroller.directive.ts | 96 ----- .../shared/video/modals/video-block.component.html | 45 --- .../shared/video/modals/video-block.component.scss | 6 - .../shared/video/modals/video-block.component.ts | 75 ---- .../video/modals/video-download.component.html | 108 ------ .../video/modals/video-download.component.scss | 64 ---- .../video/modals/video-download.component.ts | 208 ----------- .../video/modals/video-report.component.html | 97 ----- .../video/modals/video-report.component.scss | 27 -- .../shared/video/modals/video-report.component.ts | 163 -------- .../app/shared/video/recommendation-info.model.ts | 4 - client/src/app/shared/video/redundancy.service.ts | 73 ---- client/src/app/shared/video/sort-field.type.ts | 10 - client/src/app/shared/video/syndication.model.ts | 7 - .../video/video-actions-dropdown.component.html | 21 -- .../video/video-actions-dropdown.component.scss | 12 - .../video/video-actions-dropdown.component.ts | 276 -------------- client/src/app/shared/video/video-details.model.ts | 64 ---- client/src/app/shared/video/video-edit.model.ts | 123 ------- .../shared/video/video-miniature.component.html | 66 ---- .../shared/video/video-miniature.component.scss | 200 ---------- .../app/shared/video/video-miniature.component.ts | 285 -------------- .../shared/video/video-thumbnail.component.html | 33 -- .../shared/video/video-thumbnail.component.scss | 74 ---- .../app/shared/video/video-thumbnail.component.ts | 63 ---- client/src/app/shared/video/video.model.ts | 182 --------- client/src/app/shared/video/video.service.ts | 409 --------------------- .../shared/video/videos-selection.component.html | 30 -- .../shared/video/videos-selection.component.scss | 57 --- .../app/shared/video/videos-selection.component.ts | 124 ------- 36 files changed, 3480 deletions(-) delete mode 100644 client/src/app/shared/video/abstract-video-list.html delete mode 100644 client/src/app/shared/video/abstract-video-list.scss delete mode 100644 client/src/app/shared/video/abstract-video-list.ts delete mode 100644 client/src/app/shared/video/feed.component.html delete mode 100644 client/src/app/shared/video/feed.component.scss delete mode 100644 client/src/app/shared/video/feed.component.ts delete mode 100644 client/src/app/shared/video/infinite-scroller.directive.ts delete mode 100644 client/src/app/shared/video/modals/video-block.component.html delete mode 100644 client/src/app/shared/video/modals/video-block.component.scss delete mode 100644 client/src/app/shared/video/modals/video-block.component.ts delete mode 100644 client/src/app/shared/video/modals/video-download.component.html delete mode 100644 client/src/app/shared/video/modals/video-download.component.scss delete mode 100644 client/src/app/shared/video/modals/video-download.component.ts delete mode 100644 client/src/app/shared/video/modals/video-report.component.html delete mode 100644 client/src/app/shared/video/modals/video-report.component.scss delete mode 100644 client/src/app/shared/video/modals/video-report.component.ts delete mode 100644 client/src/app/shared/video/recommendation-info.model.ts delete mode 100644 client/src/app/shared/video/redundancy.service.ts delete mode 100644 client/src/app/shared/video/sort-field.type.ts delete mode 100644 client/src/app/shared/video/syndication.model.ts delete mode 100644 client/src/app/shared/video/video-actions-dropdown.component.html delete mode 100644 client/src/app/shared/video/video-actions-dropdown.component.scss delete mode 100644 client/src/app/shared/video/video-actions-dropdown.component.ts delete mode 100644 client/src/app/shared/video/video-details.model.ts delete mode 100644 client/src/app/shared/video/video-edit.model.ts delete mode 100644 client/src/app/shared/video/video-miniature.component.html delete mode 100644 client/src/app/shared/video/video-miniature.component.scss delete mode 100644 client/src/app/shared/video/video-miniature.component.ts delete mode 100644 client/src/app/shared/video/video-thumbnail.component.html delete mode 100644 client/src/app/shared/video/video-thumbnail.component.scss delete mode 100644 client/src/app/shared/video/video-thumbnail.component.ts delete mode 100644 client/src/app/shared/video/video.model.ts delete mode 100644 client/src/app/shared/video/video.service.ts delete mode 100644 client/src/app/shared/video/videos-selection.component.html delete mode 100644 client/src/app/shared/video/videos-selection.component.scss delete mode 100644 client/src/app/shared/video/videos-selection.component.ts (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html deleted file mode 100644 index 1e919ee72..000000000 --- a/client/src/app/shared/video/abstract-video-list.html +++ /dev/null @@ -1,49 +0,0 @@ -
-
-

-
- {{ titlePage }} -
- -

- - - -
- - -
-
- -
No results.
-
- -

- {{ getCurrentGroupedDateLabel(video) }} -

- -
- - -
-
-
-
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss deleted file mode 100644 index 7f23098aa..000000000 --- a/client/src/app/shared/video/abstract-video-list.scss +++ /dev/null @@ -1,75 +0,0 @@ -@import '_bootstrap-variables'; -@import '_variables'; -@import '_mixins'; -@import '_miniature'; - -.videos-header { - display: flex; - justify-content: space-between; - align-items: baseline; - - .title-page.title-page-single { - display: flex; - - my-feed { - display: inline-block; - top: 1px; - margin-left: 5px; - width: max-content; - opacity: 0; - transition: ease-in .2s opacity; - } - &:hover my-feed { - opacity: 1; - } - } - - .action-block { - a button { - @include peertube-button; - @include grey-button; - @include button-with-icon(18px, 3px, -1px); - } - } - - .moderation-block { - display: flex; - flex-grow: 1; - justify-content: flex-end; - align-items: center; - } -} - -.date-title { - font-size: 16px; - font-weight: $font-semibold; - margin-bottom: 20px; - margin-top: -10px; - - // make the element span a full grid row within .videos grid - grid-column: 1 / -1; - - &:not(:first-child) { - margin-top: .5rem; - padding-top: 20px; - border-top: 1px solid $separator-border-color; - } -} - -.margin-content { - @include fluid-videos-miniature-layout; -} - -@media screen and (max-width: $mobile-view) { - .videos-header { - flex-direction: column; - align-items: center; - height: auto; - margin-bottom: 10px; - - .title-page { - margin-bottom: 10px; - margin-right: 0px; - } - } -} diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts deleted file mode 100644 index 0bc339ff6..000000000 --- a/client/src/app/shared/video/abstract-video-list.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { fromEvent, Observable, of, Subject, Subscription } from 'rxjs' -import { debounceTime, tap, throttleTime, switchMap } from 'rxjs/operators' -import { OnDestroy, OnInit } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { Notifier, ServerService } from '@app/core' -import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' -import { GlobalIconName } from '@app/shared/images/global-icon.component' -import { ScreenService } from '@app/shared/misc/screen.service' -import { Syndication } from '@app/shared/video/syndication.model' -import { MiniatureDisplayOptions, OwnerDisplayType } from '@app/shared/video/video-miniature.component' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' -import { ServerConfig } from '@shared/models' -import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' -import { AuthService } from '../../core/auth' -import { LocalStorageService } from '../misc/storage.service' -import { ComponentPaginationLight } from '../rest/component-pagination.model' -import { User, UserService } from '../users' -import { VideoSortField } from './sort-field.type' -import { Video } from './video.model' - -enum GroupDate { - UNKNOWN = 0, - TODAY = 1, - YESTERDAY = 2, - LAST_WEEK = 3, - LAST_MONTH = 4, - OLDER = 5 -} - -export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { - pagination: ComponentPaginationLight = { - currentPage: 1, - itemsPerPage: 25 - } - sort: VideoSortField = '-publishedAt' - - categoryOneOf?: number[] - languageOneOf?: string[] - nsfwPolicy?: NSFWPolicyType - defaultSort: VideoSortField = '-publishedAt' - - syndicationItems: Syndication[] = [] - - loadOnInit = true - useUserVideoPreferences = false - ownerDisplayType: OwnerDisplayType = 'account' - displayModerationBlock = false - titleTooltip: string - displayVideoActions = true - groupByDate = false - - videos: Video[] = [] - hasDoneFirstQuery = false - disabled = false - - displayOptions: MiniatureDisplayOptions = { - date: true, - views: true, - by: true, - avatar: false, - privacyLabel: true, - privacyText: false, - state: false, - blacklistInfo: false - } - - actions: { - routerLink: string - iconName: GlobalIconName - label: string - }[] = [] - - onDataSubject = new Subject() - - userMiniature: User - - protected serverConfig: ServerConfig - - protected abstract notifier: Notifier - protected abstract authService: AuthService - protected abstract userService: UserService - protected abstract route: ActivatedRoute - protected abstract serverService: ServerService - protected abstract screenService: ScreenService - protected abstract storageService: LocalStorageService - protected abstract router: Router - protected abstract i18n: I18n - abstract titlePage: string - - private resizeSubscription: Subscription - private angularState: number - - private groupedDateLabels: { [id in GroupDate]: string } - private groupedDates: { [id: number]: GroupDate } = {} - - private lastQueryLength: number - - abstract getVideosObservable (page: number): Observable<{ data: Video[] }> - - abstract generateSyndicationList (): void - - ngOnInit () { - this.serverConfig = this.serverService.getTmpConfig() - this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) - - this.groupedDateLabels = { - [GroupDate.UNKNOWN]: null, - [GroupDate.TODAY]: this.i18n('Today'), - [GroupDate.YESTERDAY]: this.i18n('Yesterday'), - [GroupDate.LAST_WEEK]: this.i18n('Last week'), - [GroupDate.LAST_MONTH]: this.i18n('Last month'), - [GroupDate.OLDER]: this.i18n('Older') - } - - // Subscribe to route changes - const routeParams = this.route.snapshot.queryParams - this.loadRouteParams(routeParams) - - this.resizeSubscription = fromEvent(window, 'resize') - .pipe(debounceTime(500)) - .subscribe(() => this.calcPageSizes()) - - this.calcPageSizes() - - const loadUserObservable = this.loadUserAndSettings() - - if (this.loadOnInit === true) { - loadUserObservable.subscribe(() => this.loadMoreVideos()) - } - - this.userService.listenAnonymousUpdate() - .pipe(switchMap(() => this.loadUserAndSettings())) - .subscribe(() => { - if (this.hasDoneFirstQuery) this.reloadVideos() - }) - - // Display avatar in mobile view - if (this.screenService.isInMobileView()) { - this.displayOptions.avatar = true - } - } - - ngOnDestroy () { - if (this.resizeSubscription) this.resizeSubscription.unsubscribe() - } - - disableForReuse () { - this.disabled = true - } - - enabledForReuse () { - this.disabled = false - } - - videoById (index: number, video: Video) { - return video.id - } - - onNearOfBottom () { - if (this.disabled) return - - // No more results - if (this.lastQueryLength !== undefined && this.lastQueryLength < this.pagination.itemsPerPage) return - - this.pagination.currentPage += 1 - - this.setScrollRouteParams() - - this.loadMoreVideos() - } - - loadMoreVideos (reset = false) { - this.getVideosObservable(this.pagination.currentPage).subscribe( - ({ data }) => { - this.hasDoneFirstQuery = true - this.lastQueryLength = data.length - - if (reset) this.videos = [] - this.videos = this.videos.concat(data) - - if (this.groupByDate) this.buildGroupedDateLabels() - - this.onMoreVideos() - - this.onDataSubject.next(data) - }, - - error => { - const message = this.i18n('Cannot load more videos. Try again later.') - - console.error(message, { error }) - this.notifier.error(message) - } - ) - } - - reloadVideos () { - this.pagination.currentPage = 1 - this.loadMoreVideos(true) - } - - toggleModerationDisplay () { - throw new Error('toggleModerationDisplay is not implemented') - } - - removeVideoFromArray (video: Video) { - this.videos = this.videos.filter(v => v.id !== video.id) - } - - buildGroupedDateLabels () { - let currentGroupedDate: GroupDate = GroupDate.UNKNOWN - - for (const video of this.videos) { - const publishedDate = video.publishedAt - - if (currentGroupedDate <= GroupDate.TODAY && isToday(publishedDate)) { - if (currentGroupedDate === GroupDate.TODAY) continue - - currentGroupedDate = GroupDate.TODAY - this.groupedDates[ video.id ] = currentGroupedDate - continue - } - - if (currentGroupedDate <= GroupDate.YESTERDAY && isYesterday(publishedDate)) { - if (currentGroupedDate === GroupDate.YESTERDAY) continue - - currentGroupedDate = GroupDate.YESTERDAY - this.groupedDates[ video.id ] = currentGroupedDate - continue - } - - if (currentGroupedDate <= GroupDate.LAST_WEEK && isLastWeek(publishedDate)) { - if (currentGroupedDate === GroupDate.LAST_WEEK) continue - - currentGroupedDate = GroupDate.LAST_WEEK - this.groupedDates[ video.id ] = currentGroupedDate - continue - } - - if (currentGroupedDate <= GroupDate.LAST_MONTH && isLastMonth(publishedDate)) { - if (currentGroupedDate === GroupDate.LAST_MONTH) continue - - currentGroupedDate = GroupDate.LAST_MONTH - this.groupedDates[ video.id ] = currentGroupedDate - continue - } - - if (currentGroupedDate <= GroupDate.OLDER) { - if (currentGroupedDate === GroupDate.OLDER) continue - - currentGroupedDate = GroupDate.OLDER - this.groupedDates[ video.id ] = currentGroupedDate - } - } - } - - getCurrentGroupedDateLabel (video: Video) { - if (this.groupByDate === false) return undefined - - return this.groupedDateLabels[this.groupedDates[video.id]] - } - - // On videos hook for children that want to do something - protected onMoreVideos () { /* empty */ } - - protected loadRouteParams (routeParams: { [ key: string ]: any }) { - this.sort = routeParams[ 'sort' ] as VideoSortField || this.defaultSort - this.categoryOneOf = routeParams[ 'categoryOneOf' ] - this.angularState = routeParams[ 'a-state' ] - } - - private calcPageSizes () { - if (this.screenService.isInMobileView()) { - this.pagination.itemsPerPage = 5 - } - } - - private setScrollRouteParams () { - // Already set - if (this.angularState) return - - this.angularState = 42 - - const queryParams = { - 'a-state': this.angularState, - categoryOneOf: this.categoryOneOf - } - - let path = this.router.url - if (!path || path === '/') path = this.serverConfig.instance.defaultClientRoute - - this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' }) - } - - private loadUserAndSettings () { - return this.userService.getAnonymousOrLoggedUser() - .pipe(tap(user => { - this.userMiniature = user - - if (!this.useUserVideoPreferences) return - - this.languageOneOf = user.videoLanguages - this.nsfwPolicy = user.nsfwPolicy - })) - } -} diff --git a/client/src/app/shared/video/feed.component.html b/client/src/app/shared/video/feed.component.html deleted file mode 100644 index ac0b1f454..000000000 --- a/client/src/app/shared/video/feed.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - - - - {{ item.label }} - -
diff --git a/client/src/app/shared/video/feed.component.scss b/client/src/app/shared/video/feed.component.scss deleted file mode 100644 index 34dd0e937..000000000 --- a/client/src/app/shared/video/feed.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.video-feed { - width: min-content; - - a { - color: black; - display: block; - } - - my-global-icon { - cursor: pointer; - width: 12px; - position: relative; - top: -2px; - - @include apply-svg-color(pvar(--mainForegroundColor)) - } -} diff --git a/client/src/app/shared/video/feed.component.ts b/client/src/app/shared/video/feed.component.ts deleted file mode 100644 index 12507458f..000000000 --- a/client/src/app/shared/video/feed.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, Input } from '@angular/core' -import { Syndication } from '@app/shared/video/syndication.model' - -@Component({ - selector: 'my-feed', - styleUrls: [ './feed.component.scss' ], - templateUrl: './feed.component.html' -}) -export class FeedComponent { - @Input() syndicationItems: Syndication[] -} diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts deleted file mode 100644 index f09c3d1fc..000000000 --- a/client/src/app/shared/video/infinite-scroller.directive.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' -import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' -import { fromEvent, Observable, Subscription } from 'rxjs' - -@Directive({ - selector: '[myInfiniteScroller]' -}) -export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterContentChecked { - @Input() percentLimit = 70 - @Input() autoInit = false - @Input() onItself = false - @Input() dataObservable: Observable - - @Output() nearOfBottom = new EventEmitter() - - private decimalLimit = 0 - private lastCurrentBottom = -1 - private scrollDownSub: Subscription - private container: HTMLElement - - private checkScroll = false - - constructor (private el: ElementRef) { - this.decimalLimit = this.percentLimit / 100 - } - - ngAfterContentChecked () { - if (this.checkScroll) { - this.checkScroll = false - - console.log('Checking if the initial state has a scroll.') - - if (this.hasScroll() === false) this.nearOfBottom.emit() - } - } - - ngOnInit () { - if (this.autoInit === true) return this.initialize() - } - - ngOnDestroy () { - if (this.scrollDownSub) this.scrollDownSub.unsubscribe() - } - - initialize () { - this.container = this.onItself - ? this.el.nativeElement - : document.documentElement - - // Emit the last value - const throttleOptions = { leading: true, trailing: true } - - const scrollableElement = this.onItself ? this.container : window - const scrollObservable = fromEvent(scrollableElement, 'scroll') - .pipe( - startWith(true), - throttleTime(200, undefined, throttleOptions), - map(() => this.getScrollInfo()), - distinctUntilChanged((o1, o2) => o1.current === o2.current), - share() - ) - - // Scroll Down - this.scrollDownSub = scrollObservable - .pipe( - filter(({ current }) => this.isScrollingDown(current)), - filter(({ current, maximumScroll }) => (current / maximumScroll) > this.decimalLimit) - ) - .subscribe(() => this.nearOfBottom.emit()) - - if (this.dataObservable) { - this.dataObservable - .pipe(filter(d => d.length !== 0)) - .subscribe(() => this.checkScroll = true) - } - } - - private getScrollInfo () { - return { current: this.container.scrollTop, maximumScroll: this.getMaximumScroll() } - } - - private getMaximumScroll () { - return this.container.scrollHeight - window.innerHeight - } - - private hasScroll () { - return this.getMaximumScroll() > 0 - } - - private isScrollingDown (current: number) { - const result = this.lastCurrentBottom < current - - this.lastCurrentBottom = current - return result - } -} diff --git a/client/src/app/shared/video/modals/video-block.component.html b/client/src/app/shared/video/modals/video-block.component.html deleted file mode 100644 index 5e73d66c5..000000000 --- a/client/src/app/shared/video/modals/video-block.component.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/client/src/app/shared/video/modals/video-block.component.scss b/client/src/app/shared/video/modals/video-block.component.scss deleted file mode 100644 index afcdb9a16..000000000 --- a/client/src/app/shared/video/modals/video-block.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import 'variables'; -@import 'mixins'; - -textarea { - @include peertube-textarea(100%, 100px); -} diff --git a/client/src/app/shared/video/modals/video-block.component.ts b/client/src/app/shared/video/modals/video-block.component.ts deleted file mode 100644 index 1a25e0578..000000000 --- a/client/src/app/shared/video/modals/video-block.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' -import { Notifier, RedirectService } from '@app/core' -import { VideoBlockService } from '../../video-block' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' -import { FormReactive, VideoBlockValidatorsService } from '@app/shared/forms' -import { Video } from '@app/shared/video/video.model' - -@Component({ - selector: 'my-video-block', - templateUrl: './video-block.component.html', - styleUrls: [ './video-block.component.scss' ] -}) -export class VideoBlockComponent extends FormReactive implements OnInit { - @Input() video: Video = null - - @ViewChild('modal', { static: true }) modal: NgbModal - - @Output() videoBlocked = new EventEmitter() - - error: string = null - - private openedModal: NgbModalRef - - constructor ( - protected formValidatorService: FormValidatorService, - private modalService: NgbModal, - private videoBlockValidatorsService: VideoBlockValidatorsService, - private videoBlocklistService: VideoBlockService, - private notifier: Notifier, - private i18n: I18n - ) { - super() - } - - ngOnInit () { - const defaultValues = { unfederate: 'true' } - - this.buildForm({ - reason: this.videoBlockValidatorsService.VIDEO_BLOCK_REASON, - unfederate: null - }, defaultValues) - } - - show () { - this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) - } - - hide () { - this.openedModal.close() - this.openedModal = null - } - - block () { - const reason = this.form.value[ 'reason' ] || undefined - const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined - - this.videoBlocklistService.blockVideo(this.video.id, reason, unfederate) - .subscribe( - () => { - this.notifier.success(this.i18n('Video blocked.')) - this.hide() - - this.video.blacklisted = true - this.video.blockedReason = reason - - this.videoBlocked.emit() - }, - - err => this.notifier.error(err.message) - ) - } -} diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html deleted file mode 100644 index c65e371ee..000000000 --- a/client/src/app/shared/video/modals/video-download.component.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - diff --git a/client/src/app/shared/video/modals/video-download.component.scss b/client/src/app/shared/video/modals/video-download.component.scss deleted file mode 100644 index b09078bea..000000000 --- a/client/src/app/shared/video/modals/video-download.component.scss +++ /dev/null @@ -1,64 +0,0 @@ -@import 'variables'; -@import 'mixins'; - -.peertube-select-container { - @include peertube-select-container(100px); - - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: none; - - select { - height: inherit; - } -} - -#dropdownDownloadType { - cursor: pointer; -} - -.download-type { - margin-top: 30px; - - .peertube-radio-container { - @include peertube-radio-container; - - display: inline-block; - margin-right: 30px; - } -} - -.file-metadata { - padding: 1rem; -} - -.file-metadata .metadata-attribute { - font-size: 13px; - display: block; - margin-bottom: 12px; - - .metadata-attribute-label { - min-width: 142px; - padding-right: 5px; - display: inline-block; - color: pvar(--greyForegroundColor); - font-weight: $font-bold; - } - - a.metadata-attribute-value { - @include disable-default-a-behaviour; - color: pvar(--mainForegroundColor); - - &:hover { - opacity: 0.9; - } - } - - &.metadata-attribute-tags { - .metadata-attribute-value:not(:nth-child(2)) { - &::before { - content: ', ' - } - } - } -} diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts deleted file mode 100644 index d77187821..000000000 --- a/client/src/app/shared/video/modals/video-download.component.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { Component, ElementRef, ViewChild } from '@angular/core' -import { VideoDetails } from '../../../shared/video/video-details.model' -import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { AuthService, Notifier } from '@app/core' -import { VideoPrivacy, VideoCaption, VideoFile } from '@shared/models' -import { FfprobeFormat, FfprobeStream } from 'fluent-ffmpeg' -import { mapValues, pick } from 'lodash-es' -import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe' -import { BytesPipe } from 'ngx-pipes' -import { VideoService } from '../video.service' - -type DownloadType = 'video' | 'subtitles' -type FileMetadata = { [key: string]: { label: string, value: string }} - -@Component({ - selector: 'my-video-download', - templateUrl: './video-download.component.html', - styleUrls: [ './video-download.component.scss' ] -}) -export class VideoDownloadComponent { - @ViewChild('modal', { static: true }) modal: ElementRef - - downloadType: 'direct' | 'torrent' = 'torrent' - resolutionId: number | string = -1 - subtitleLanguageId: string - - video: VideoDetails - videoFile: VideoFile - videoFileMetadataFormat: FileMetadata - videoFileMetadataVideoStream: FileMetadata | undefined - videoFileMetadataAudioStream: FileMetadata | undefined - videoCaptions: VideoCaption[] - activeModal: NgbActiveModal - - type: DownloadType = 'video' - - private bytesPipe: BytesPipe - private numbersPipe: NumberFormatterPipe - - constructor ( - private notifier: Notifier, - private modalService: NgbModal, - private videoService: VideoService, - private auth: AuthService, - private i18n: I18n - ) { - this.bytesPipe = new BytesPipe() - this.numbersPipe = new NumberFormatterPipe() - } - - get typeText () { - return this.type === 'video' - ? this.i18n('video') - : this.i18n('subtitles') - } - - getVideoFiles () { - if (!this.video) return [] - - return this.video.getFiles() - } - - show (video: VideoDetails, videoCaptions?: VideoCaption[]) { - this.video = video - this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined - - this.activeModal = this.modalService.open(this.modal, { centered: true }) - - this.resolutionId = this.getVideoFiles()[0].resolution.id - this.onResolutionIdChange() - if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id - } - - onClose () { - this.video = undefined - this.videoCaptions = undefined - } - - download () { - window.location.assign(this.getLink()) - this.activeModal.close() - } - - getLink () { - return this.type === 'subtitles' && this.videoCaptions - ? this.getSubtitlesLink() - : this.getVideoFileLink() - } - - async onResolutionIdChange () { - this.videoFile = this.getVideoFile() - if (this.videoFile.metadata || !this.videoFile.metadataUrl) return - - await this.hydrateMetadataFromMetadataUrl(this.videoFile) - - this.videoFileMetadataFormat = this.videoFile - ? this.getMetadataFormat(this.videoFile.metadata.format) - : undefined - this.videoFileMetadataVideoStream = this.videoFile - ? this.getMetadataStream(this.videoFile.metadata.streams, 'video') - : undefined - this.videoFileMetadataAudioStream = this.videoFile - ? this.getMetadataStream(this.videoFile.metadata.streams, 'audio') - : undefined - } - - getVideoFile () { - // HTML select send us a string, so convert it to a number - this.resolutionId = parseInt(this.resolutionId.toString(), 10) - - const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) - if (!file) { - console.error('Could not find file with resolution %d.', this.resolutionId) - return - } - return file - } - - getVideoFileLink () { - const file = this.videoFile - if (!file) return - - const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL - ? '?access_token=' + this.auth.getAccessToken() - : '' - - switch (this.downloadType) { - case 'direct': - return file.fileDownloadUrl + suffix - - case 'torrent': - return file.torrentDownloadUrl + suffix - } - } - - getSubtitlesLink () { - return window.location.origin + this.videoCaptions.find(caption => caption.language.id === this.subtitleLanguageId).captionPath - } - - activateCopiedMessage () { - this.notifier.success(this.i18n('Copied')) - } - - switchToType (type: DownloadType) { - this.type = type - } - - getMetadataFormat (format: FfprobeFormat) { - const keyToTranslateFunction = { - 'encoder': (value: string) => ({ label: this.i18n('Encoder'), value }), - 'format_long_name': (value: string) => ({ label: this.i18n('Format name'), value }), - 'size': (value: number) => ({ label: this.i18n('Size'), value: this.bytesPipe.transform(value, 2) }), - 'bit_rate': (value: number) => ({ - label: this.i18n('Bitrate'), - value: `${this.numbersPipe.transform(value)}bps` - }) - } - - // flattening format - const sanitizedFormat = Object.assign(format, format.tags) - delete sanitizedFormat.tags - - return mapValues( - pick(sanitizedFormat, Object.keys(keyToTranslateFunction)), - (val, key) => keyToTranslateFunction[key](val) - ) - } - - getMetadataStream (streams: FfprobeStream[], type: 'video' | 'audio') { - const stream = streams.find(s => s.codec_type === type) - if (!stream) return undefined - - let keyToTranslateFunction = { - 'codec_long_name': (value: string) => ({ label: this.i18n('Codec'), value }), - 'profile': (value: string) => ({ label: this.i18n('Profile'), value }), - 'bit_rate': (value: number) => ({ - label: this.i18n('Bitrate'), - value: `${this.numbersPipe.transform(value)}bps` - }) - } - - if (type === 'video') { - keyToTranslateFunction = Object.assign(keyToTranslateFunction, { - 'width': (value: number) => ({ label: this.i18n('Resolution'), value: `${value}x${stream.height}` }), - 'display_aspect_ratio': (value: string) => ({ label: this.i18n('Aspect ratio'), value }), - 'avg_frame_rate': (value: string) => ({ label: this.i18n('Average frame rate'), value }), - 'pix_fmt': (value: string) => ({ label: this.i18n('Pixel format'), value }) - }) - } else { - keyToTranslateFunction = Object.assign(keyToTranslateFunction, { - 'sample_rate': (value: number) => ({ label: this.i18n('Sample rate'), value }), - 'channel_layout': (value: number) => ({ label: this.i18n('Channel Layout'), value }) - }) - } - - return mapValues( - pick(stream, Object.keys(keyToTranslateFunction)), - (val, key) => keyToTranslateFunction[key](val) - ) - } - - private hydrateMetadataFromMetadataUrl (file: VideoFile) { - const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) - observable.subscribe(res => file.metadata = res) - return observable.toPromise() - } -} diff --git a/client/src/app/shared/video/modals/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html deleted file mode 100644 index d6beb6d2a..000000000 --- a/client/src/app/shared/video/modals/video-report.component.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - diff --git a/client/src/app/shared/video/modals/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss deleted file mode 100644 index b2606cbd8..000000000 --- a/client/src/app/shared/video/modals/video-report.component.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import 'variables'; -@import 'mixins'; - -.information { - margin-bottom: 20px; -} - -textarea { - @include peertube-textarea(100%, 100px); -} - -.start-at, -.stop-at { - width: 300px; - display: flex; - align-items: center; - - my-timestamp-input { - margin-left: 10px; - } -} - -.screenratio { - @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { - left: 0; - }; -} diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts deleted file mode 100644 index c2d441bba..000000000 --- a/client/src/app/shared/video/modals/video-report.component.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Component, Input, OnInit, ViewChild } from '@angular/core' -import { Notifier } from '@app/core' -import { FormReactive } from '../../../shared/forms' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' -import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' -import { VideoAbuseService } from '@app/shared/video-abuse' -import { Video } from '@app/shared/video/video.model' -import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' -import { DomSanitizer, SafeHtml } from '@angular/platform-browser' -import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model' -import { mapValues, pickBy } from 'lodash-es' - -@Component({ - selector: 'my-video-report', - templateUrl: './video-report.component.html', - styleUrls: [ './video-report.component.scss' ] -}) -export class VideoReportComponent extends FormReactive implements OnInit { - @Input() video: Video = null - - @ViewChild('modal', { static: true }) modal: NgbModal - - error: string = null - predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] - embedHtml: SafeHtml - - private openedModal: NgbModalRef - - constructor ( - protected formValidatorService: FormValidatorService, - private modalService: NgbModal, - private videoAbuseValidatorsService: VideoAbuseValidatorsService, - private videoAbuseService: VideoAbuseService, - private notifier: Notifier, - private sanitizer: DomSanitizer, - private i18n: I18n - ) { - super() - } - - get currentHost () { - return window.location.host - } - - get originHost () { - if (this.isRemoteVideo()) { - return this.video.account.host - } - - return '' - } - - get timestamp () { - return this.form.get('timestamp').value - } - - getVideoEmbed () { - return this.sanitizer.bypassSecurityTrustHtml( - buildVideoEmbed( - buildVideoLink({ - baseUrl: this.video.embedUrl, - title: false, - warningTitle: false - }) - ) - ) - } - - ngOnInit () { - this.buildForm({ - reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON, - predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null), - timestamp: { - hasStart: null, - startAt: null, - hasEnd: null, - endAt: null - } - }) - - this.predefinedReasons = [ - { - id: 'violentOrRepulsive', - label: this.i18n('Violent or repulsive'), - help: this.i18n('Contains offensive, violent, or coarse language or iconography.') - }, - { - id: 'hatefulOrAbusive', - label: this.i18n('Hateful or abusive'), - help: this.i18n('Contains abusive, racist or sexist language or iconography.') - }, - { - id: 'spamOrMisleading', - label: this.i18n('Spam, ad or false news'), - help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') - }, - { - id: 'privacy', - label: this.i18n('Privacy breach or doxxing'), - help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') - }, - { - id: 'rights', - label: this.i18n('Intellectual property violation'), - help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') - }, - { - id: 'serverRules', - label: this.i18n('Breaks server rules'), - description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') - }, - { - id: 'thumbnails', - label: this.i18n('Thumbnails'), - help: this.i18n('The above can only be seen in thumbnails.') - }, - { - id: 'captions', - label: this.i18n('Captions'), - help: this.i18n('The above can only be seen in captions (please describe which).') - } - ] - - this.embedHtml = this.getVideoEmbed() - } - - show () { - this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) - } - - hide () { - this.openedModal.close() - this.openedModal = null - } - - report () { - const reason = this.form.get('reason').value - const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[] - const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value - - this.videoAbuseService.reportVideo({ - id: this.video.id, - reason, - predefinedReasons, - startAt: hasStart && startAt ? startAt : undefined, - endAt: hasEnd && endAt ? endAt : undefined - }).subscribe( - () => { - this.notifier.success(this.i18n('Video reported.')) - this.hide() - }, - - err => this.notifier.error(err.message) - ) - } - - isRemoteVideo () { - return !this.video.isLocal - } -} diff --git a/client/src/app/shared/video/recommendation-info.model.ts b/client/src/app/shared/video/recommendation-info.model.ts deleted file mode 100644 index 0233563bb..000000000 --- a/client/src/app/shared/video/recommendation-info.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RecommendationInfo { - uuid: string - tags?: string[] -} diff --git a/client/src/app/shared/video/redundancy.service.ts b/client/src/app/shared/video/redundancy.service.ts deleted file mode 100644 index fb918d73b..000000000 --- a/client/src/app/shared/video/redundancy.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { catchError, map, toArray } from 'rxjs/operators' -import { HttpClient, HttpParams } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { RestExtractor, RestPagination, RestService } from '@app/shared/rest' -import { SortMeta } from 'primeng/api' -import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' -import { concat, Observable } from 'rxjs' -import { environment } from '../../../environments/environment' - -@Injectable() -export class RedundancyService { - static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy' - - constructor ( - private authHttp: HttpClient, - private restService: RestService, - private restExtractor: RestExtractor - ) { } - - updateRedundancy (host: string, redundancyAllowed: boolean) { - const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host - - const body = { redundancyAllowed } - - return this.authHttp.put(url, body) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - listVideoRedundancies (options: { - pagination: RestPagination, - sort: SortMeta, - target?: VideoRedundanciesTarget - }): Observable> { - const { pagination, sort, target } = options - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (target) params = params.append('target', target) - - return this.authHttp.get>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params }) - .pipe( - catchError(res => this.restExtractor.handleError(res)) - ) - } - - addVideoRedundancy (video: Video) { - return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id }) - .pipe( - catchError(res => this.restExtractor.handleError(res)) - ) - } - - removeVideoRedundancies (redundancy: VideoRedundancy) { - const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id) - .concat(redundancy.redundancies.files.map(r => r.id)) - .map(id => this.removeRedundancy(id)) - - return concat(...observables) - .pipe(toArray()) - } - - private removeRedundancy (redundancyId: number) { - return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) - } -} diff --git a/client/src/app/shared/video/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts deleted file mode 100644 index 65b24d946..000000000 --- a/client/src/app/shared/video/sort-field.type.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type VideoSortField = 'name' | '-name' - | 'duration' | '-duration' - | 'publishedAt' | '-publishedAt' - | 'createdAt' | '-createdAt' - | 'views' | '-views' - | 'likes' | '-likes' - | 'trending' | '-trending' - -export type CommentSortField = 'createdAt' | '-createdAt' - | 'totalReplies' | '-totalReplies' diff --git a/client/src/app/shared/video/syndication.model.ts b/client/src/app/shared/video/syndication.model.ts deleted file mode 100644 index c59ab01e8..000000000 --- a/client/src/app/shared/video/syndication.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' - -export interface Syndication { - format: FeedFormat, - label: string, - url: string -} diff --git a/client/src/app/shared/video/video-actions-dropdown.component.html b/client/src/app/shared/video/video-actions-dropdown.component.html deleted file mode 100644 index 3c8271b65..000000000 --- a/client/src/app/shared/video/video-actions-dropdown.component.html +++ /dev/null @@ -1,21 +0,0 @@ - - -
- - -
- -
-
- - - - - - -
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.scss b/client/src/app/shared/video/video-actions-dropdown.component.scss deleted file mode 100644 index 67d7ee86a..000000000 --- a/client/src/app/shared/video/video-actions-dropdown.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -.playlist-dropdown { - position: absolute; - - .anchor { - display: block; - opacity: 0; - } -} - -::ng-deep .icon-playlist-add { - left: 2px; -} diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts deleted file mode 100644 index 1f5763610..000000000 --- a/client/src/app/shared/video/video-actions-dropdown.component.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { DropdownAction, DropdownButtonSize, DropdownDirection } from '@app/shared/buttons/action-dropdown.component' -import { AuthService, ConfirmService, Notifier } from '@app/core' -import { Video } from '@app/shared/video/video.model' -import { VideoService } from '@app/shared/video/video.service' -import { VideoDetails } from '@app/shared/video/video-details.model' -import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' -import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' -import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' -import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' -import { VideoBlockComponent } from '@app/shared/video/modals/video-block.component' -import { VideoBlockService } from '@app/shared/video-block' -import { ScreenService } from '@app/shared/misc/screen.service' -import { VideoCaption } from '@shared/models' -import { RedundancyService } from '@app/shared/video/redundancy.service' - -export type VideoActionsDisplayType = { - playlist?: boolean - download?: boolean - update?: boolean - blacklist?: boolean - delete?: boolean - report?: boolean - duplicate?: boolean -} - -@Component({ - selector: 'my-video-actions-dropdown', - templateUrl: './video-actions-dropdown.component.html', - styleUrls: [ './video-actions-dropdown.component.scss' ] -}) -export class VideoActionsDropdownComponent implements OnChanges { - @ViewChild('playlistDropdown') playlistDropdown: NgbDropdown - @ViewChild('playlistAdd') playlistAdd: VideoAddToPlaylistComponent - - @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent - @ViewChild('videoReportModal') videoReportModal: VideoReportComponent - @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent - - @Input() video: Video | VideoDetails - @Input() videoCaptions: VideoCaption[] = [] - - @Input() displayOptions: VideoActionsDisplayType = { - playlist: false, - download: true, - update: true, - blacklist: true, - delete: true, - report: true, - duplicate: true - } - @Input() placement = 'left' - - @Input() label: string - - @Input() buttonStyled = false - @Input() buttonSize: DropdownButtonSize = 'normal' - @Input() buttonDirection: DropdownDirection = 'vertical' - - @Output() videoRemoved = new EventEmitter() - @Output() videoUnblocked = new EventEmitter() - @Output() videoBlocked = new EventEmitter() - @Output() modalOpened = new EventEmitter() - - videoActions: DropdownAction<{ video: Video }>[][] = [] - - private loaded = false - - constructor ( - private authService: AuthService, - private notifier: Notifier, - private confirmService: ConfirmService, - private videoBlocklistService: VideoBlockService, - private screenService: ScreenService, - private videoService: VideoService, - private redundancyService: RedundancyService, - private i18n: I18n - ) { } - - get user () { - return this.authService.getUser() - } - - ngOnChanges () { - if (this.loaded) { - this.loaded = false - this.playlistAdd.reload() - } - - this.buildActions() - } - - isUserLoggedIn () { - return this.authService.isLoggedIn() - } - - loadDropdownInformation () { - if (!this.isUserLoggedIn() || this.loaded === true) return - - this.loaded = true - - if (this.displayOptions.playlist) this.playlistAdd.load() - } - - /* Show modals */ - - showDownloadModal () { - this.modalOpened.emit() - - this.videoDownloadModal.show(this.video as VideoDetails, this.videoCaptions) - } - - showReportModal () { - this.modalOpened.emit() - - this.videoReportModal.show() - } - - showBlockModal () { - this.modalOpened.emit() - - this.videoBlockModal.show() - } - - /* Actions checker */ - - isVideoUpdatable () { - return this.video.isUpdatableBy(this.user) - } - - isVideoRemovable () { - return this.video.isRemovableBy(this.user) - } - - isVideoBlockable () { - return this.video.isBlockableBy(this.user) - } - - isVideoUnblockable () { - return this.video.isUnblockableBy(this.user) - } - - isVideoDownloadable () { - return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled - } - - canVideoBeDuplicated () { - return this.video.canBeDuplicatedBy(this.user) - } - - /* Action handlers */ - - async unblockVideo () { - const confirmMessage = this.i18n( - 'Do you really want to unblock this video? It will be available again in the videos list.' - ) - - const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblock')) - if (res === false) return - - this.videoBlocklistService.unblockVideo(this.video.id).subscribe( - () => { - this.notifier.success(this.i18n('Video {{name}} unblocked.', { name: this.video.name })) - - this.video.blacklisted = false - this.video.blockedReason = null - - this.videoUnblocked.emit() - }, - - err => this.notifier.error(err.message) - ) - } - - async removeVideo () { - this.modalOpened.emit() - - const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete')) - if (res === false) return - - this.videoService.removeVideo(this.video.id) - .subscribe( - () => { - this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })) - - this.videoRemoved.emit() - }, - - error => this.notifier.error(error.message) - ) - } - - duplicateVideo () { - this.redundancyService.addVideoRedundancy(this.video) - .subscribe( - () => { - const message = this.i18n('This video will be duplicated by your instance.') - this.notifier.success(message) - }, - - err => this.notifier.error(err.message) - ) - } - - onVideoBlocked () { - this.videoBlocked.emit() - } - - getPlaylistDropdownPlacement () { - if (this.screenService.isInSmallView()) { - return 'bottom-right' - } - - return 'bottom-left bottom-right' - } - - private buildActions () { - this.videoActions = [ - [ - { - label: this.i18n('Save to playlist'), - handler: () => this.playlistDropdown.toggle(), - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.playlist, - iconName: 'playlist-add' - } - ], - [ - { - label: this.i18n('Download'), - handler: () => this.showDownloadModal(), - isDisplayed: () => this.displayOptions.download && this.isVideoDownloadable(), - iconName: 'download' - }, - { - label: this.i18n('Update'), - linkBuilder: ({ video }) => [ '/videos/update', video.uuid ], - iconName: 'edit', - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.update && this.isVideoUpdatable() - }, - { - label: this.i18n('Block'), - handler: () => this.showBlockModal(), - iconName: 'no', - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoBlockable() - }, - { - label: this.i18n('Unblock'), - handler: () => this.unblockVideo(), - iconName: 'undo', - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblockable() - }, - { - label: this.i18n('Mirror'), - handler: () => this.duplicateVideo(), - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(), - iconName: 'cloud-download' - }, - { - label: this.i18n('Delete'), - handler: () => this.removeVideo(), - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), - iconName: 'delete' - } - ], - [ - { - label: this.i18n('Report'), - handler: () => this.showReportModal(), - isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.report, - iconName: 'alert' - } - ] - ] - } -} diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts deleted file mode 100644 index 14347a109..000000000 --- a/client/src/app/shared/video/video-details.model.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' -import { Video } from '../../shared/video/video.model' -import { Account } from '@app/shared/account/account.model' -import { VideoChannel } from '@app/shared/video-channel/video-channel.model' -import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model' -import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type' - -export class VideoDetails extends Video implements VideoDetailsServerModel { - descriptionPath: string - support: string - channel: VideoChannel - tags: string[] - files: VideoFile[] - account: Account - commentsEnabled: boolean - downloadEnabled: boolean - - waitTranscoding: boolean - state: VideoConstant - - likesPercent: number - dislikesPercent: number - - trackerUrls: string[] - - streamingPlaylists: VideoStreamingPlaylist[] - - constructor (hash: VideoDetailsServerModel, translations = {}) { - super(hash, translations) - - this.descriptionPath = hash.descriptionPath - this.files = hash.files - this.channel = new VideoChannel(hash.channel) - this.account = new Account(hash.account) - this.tags = hash.tags - this.support = hash.support - this.commentsEnabled = hash.commentsEnabled - this.downloadEnabled = hash.downloadEnabled - - this.trackerUrls = hash.trackerUrls - this.streamingPlaylists = hash.streamingPlaylists - - this.buildLikeAndDislikePercents() - } - - buildLikeAndDislikePercents () { - this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 - this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 - } - - getHlsPlaylist () { - return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) - } - - hasHlsPlaylist () { - return !!this.getHlsPlaylist() - } - - getFiles () { - if (this.files.length === 0) return this.getHlsPlaylist().files - - return this.files - } -} diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts deleted file mode 100644 index 67d8e7711..000000000 --- a/client/src/app/shared/video/video-edit.model.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' -import { VideoUpdate } from '../../../../../shared/models/videos' -import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' -import { Video } from '../../../../../shared/models/videos/video.model' - -export class VideoEdit implements VideoUpdate { - static readonly SPECIAL_SCHEDULED_PRIVACY = -1 - - category: number - licence: number - language: string - description: string - name: string - tags: string[] - nsfw: boolean - commentsEnabled: boolean - downloadEnabled: boolean - waitTranscoding: boolean - channelId: number - privacy: VideoPrivacy - support: string - thumbnailfile?: any - previewfile?: any - thumbnailUrl: string - previewUrl: string - uuid?: string - id?: number - scheduleUpdate?: VideoScheduleUpdate - originallyPublishedAt?: Date | string - - constructor ( - video?: Video & { - tags: string[], - commentsEnabled: boolean, - downloadEnabled: boolean, - support: string, - thumbnailUrl: string, - previewUrl: string - }) { - if (video) { - this.id = video.id - this.uuid = video.uuid - this.category = video.category.id - this.licence = video.licence.id - this.language = video.language.id - this.description = video.description - this.name = video.name - this.tags = video.tags - this.nsfw = video.nsfw - this.commentsEnabled = video.commentsEnabled - this.downloadEnabled = video.downloadEnabled - this.waitTranscoding = video.waitTranscoding - this.channelId = video.channel.id - this.privacy = video.privacy.id - this.support = video.support - this.thumbnailUrl = video.thumbnailUrl - this.previewUrl = video.previewUrl - - this.scheduleUpdate = video.scheduledUpdate - this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null - } - } - - patch (values: { [ id: string ]: string }) { - Object.keys(values).forEach((key) => { - this[ key ] = values[ key ] - }) - - // If schedule publication, the video is private and will be changed to public privacy - if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { - const updateAt = new Date(values['schedulePublicationAt']) - updateAt.setSeconds(0) - - this.privacy = VideoPrivacy.PRIVATE - this.scheduleUpdate = { - updateAt: updateAt.toISOString(), - privacy: VideoPrivacy.PUBLIC - } - } else { - this.scheduleUpdate = null - } - - // Convert originallyPublishedAt to string so that function objectToFormData() works correctly - if (this.originallyPublishedAt) { - const originallyPublishedAt = new Date(values['originallyPublishedAt']) - this.originallyPublishedAt = originallyPublishedAt.toISOString() - } - - // Use the same file than the preview for the thumbnail - if (this.previewfile) { - this.thumbnailfile = this.previewfile - } - } - - toFormPatch () { - const json = { - category: this.category, - licence: this.licence, - language: this.language, - description: this.description, - support: this.support, - name: this.name, - tags: this.tags, - nsfw: this.nsfw, - commentsEnabled: this.commentsEnabled, - downloadEnabled: this.downloadEnabled, - waitTranscoding: this.waitTranscoding, - channelId: this.channelId, - privacy: this.privacy, - originallyPublishedAt: this.originallyPublishedAt - } - - // Special case if we scheduled an update - if (this.scheduleUpdate) { - Object.assign(json, { - privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY, - schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString()) - }) - } - - return json - } -} diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html deleted file mode 100644 index 82afc866f..000000000 --- a/client/src/app/shared/video/video-miniature.component.html +++ /dev/null @@ -1,66 +0,0 @@ -
- - Unlisted - Private - - -
-
-
- - - - -
- {{ video.name }} - - - - - - - {video.views, plural, =1 {1 view} other {{{ video.views | myNumberFormatter }} views}} - - - - - - {{ video.byVideoChannel }} - - -
- {{ video.privacy.label }} - - - {{ getStateLabel(video) }} -
-
-
- -
- Blocked - {{ video.blockedReason }} -
- -
- Sensitive -
-
- -
- - -
-
-
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss deleted file mode 100644 index 38cac5b6e..000000000 --- a/client/src/app/shared/video/video-miniature.component.scss +++ /dev/null @@ -1,200 +0,0 @@ -@import '_variables'; -@import '_mixins'; -@import '_miniature'; - -$more-button-width: 40px; -$more-margin-right: 15px; - -.video-miniature { - display: inline-flex; - flex-direction: column; - padding-bottom: $video-miniature-margin-bottom; - vertical-align: top; - - .video-bottom { - display: flex; - - .video-miniature-information { - width: $video-miniature-width - $more-button-width - $more-margin-right; - line-height: normal; - - .avatar { - margin: 10px 10px 0 0; - - img { - @include avatar(40px); - } - } - - .video-miniature-name { - @include miniature-name; - width: calc(100% - #{$more-button-width}); - } - - .video-miniature-meta { - width: calc(100% + #{$more-button-width}); - overflow: hidden; - } - - .video-miniature-created-at-views { - display: block; - font-size: 13px; - } - - .video-miniature-account, - .video-miniature-channel { - @include disable-default-a-behaviour; - @include ellipsis; - - display: block; - font-size: 13px; - color: pvar(--greyForegroundColor); - - &:hover { - color: $grey-foreground-hover-color; - } - } - - .video-info-privacy, - .video-info-blocked .blocked-label, - .video-info-nsfw { - font-weight: $font-semibold; - } - - .video-info-blocked { - color: red; - - .blocked-reason::before { - content: ' - '; - } - } - - .video-info-nsfw { - color: red; - } - } - - .video-actions { - margin-top: 3px; - width: $more-button-width; - height: 30px; - - ::ng-deep .dropdown-root:not(.show) { - opacity: 0; - } - - ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { - opacity: 1; - } - - ::ng-deep .more-icon { - opacity: .6; - - &:hover { - opacity: 1; - } - } - } - - @media screen and (max-width: $small-view) { - .video-miniature-information { - margin: 0 10px; - } - - .video-actions { - margin: 0; - top: -3px; - - ::ng-deep .dropdown-root { - opacity: 1 !important; - } - } - } - } - - &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, - &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { - opacity: 1; - } - - &.fit-width { - width: 100%; - - .video-bottom { - width: 100% !important; - - .video-miniature-information { - width: calc(100% - #{$more-button-width}) !important; - } - } - - my-video-thumbnail { - @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); - } - } - - &.display-as-row { - flex-direction: row; - padding-bottom: 0; - height: auto; - display: flex; - flex-grow: 1; - - my-video-thumbnail { - margin-right: 10px; - } - - .video-bottom { - .video-miniature-information { - @media screen and (min-width: $small-view) { - width: auto; - min-width: 500px; - } - - .video-miniature-name { - @include ellipsis-multiline(1.3em, 2); - - margin-top: 2px; - margin-bottom: 5px; - } - - .video-miniature-created-at-views, - .video-miniature-account, - .video-miniature-channel { - font-size: 95%; - width: fit-content; - } - - .video-miniature-created-at-views + .video-miniature-channel { - margin-top: 5px; - } - - .video-info-privacy { - margin-top: 5px; - } - - .video-info-blocked { - margin-top: 3px; - } - } - - .video-actions { - margin: 0; - top: -3px; - } - } - - @media screen and (max-width: $small-view) { - flex-direction: column; - height: auto; - - my-video-thumbnail { - margin-right: 0; - } - - .video-miniature-information { - min-width: initial; - } - } - } -} diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts deleted file mode 100644 index a08c3fc8d..000000000 --- a/client/src/app/shared/video/video-miniature.component.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { switchMap } from 'rxjs/operators' -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - LOCALE_ID, - OnInit, - Output -} from '@angular/core' -import { AuthService, ServerService } from '@app/core' -import { ScreenService } from '@app/shared/misc/screen.service' -import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' -import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { ServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '../../../../../shared' -import { User } from '../users' -import { Video } from './video.model' - -export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' -export type MiniatureDisplayOptions = { - date?: boolean - views?: boolean - by?: boolean - avatar?: boolean - privacyLabel?: boolean - privacyText?: boolean - state?: boolean - blacklistInfo?: boolean - nsfw?: boolean -} - -@Component({ - selector: 'my-video-miniature', - styleUrls: [ './video-miniature.component.scss' ], - templateUrl: './video-miniature.component.html', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class VideoMiniatureComponent implements OnInit { - @Input() user: User - @Input() video: Video - - @Input() ownerDisplayType: OwnerDisplayType = 'account' - @Input() displayOptions: MiniatureDisplayOptions = { - date: true, - views: true, - by: true, - avatar: false, - privacyLabel: false, - privacyText: false, - state: false, - blacklistInfo: false - } - @Input() displayAsRow = false - @Input() displayVideoActions = true - @Input() fitWidth = false - - @Input() useLazyLoadUrl = false - - @Output() videoBlocked = new EventEmitter() - @Output() videoUnblocked = new EventEmitter() - @Output() videoRemoved = new EventEmitter() - - videoActionsDisplayOptions: VideoActionsDisplayType = { - playlist: true, - download: false, - update: true, - blacklist: true, - delete: true, - report: true, - duplicate: true - } - showActions = false - serverConfig: ServerConfig - - addToWatchLaterText: string - addedToWatchLaterText: string - inWatchLaterPlaylist: boolean - channelLinkTitle = '' - - watchLaterPlaylist: { - id: number - playlistElementId?: number - } - - videoLink: any[] = [] - - private ownerDisplayTypeChosen: 'account' | 'videoChannel' - - constructor ( - private screenService: ScreenService, - private serverService: ServerService, - private i18n: I18n, - private authService: AuthService, - private videoPlaylistService: VideoPlaylistService, - private cd: ChangeDetectorRef, - @Inject(LOCALE_ID) private localeId: string - ) {} - - get isVideoBlur () { - return this.video.isVideoNSFWForUser(this.user, this.serverConfig) - } - - ngOnInit () { - this.serverConfig = this.serverService.getTmpConfig() - this.serverService.getConfig() - .subscribe(config => { - this.serverConfig = config - this.buildVideoLink() - }) - - this.setUpBy() - - this.channelLinkTitle = this.i18n( - '{{name}} (channel page)', - { name: this.video.channel.name, handle: this.video.byVideoChannel } - ) - - // We rely on mouseenter to lazy load actions - if (this.screenService.isInTouchScreen()) { - this.loadActions() - } - } - - buildVideoLink () { - if (this.useLazyLoadUrl && this.video.url) { - const remoteUriConfig = this.serverConfig.search.remoteUri - - // Redirect on the external instance if not allowed to fetch remote data - const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users - const fromPath = window.location.pathname + window.location.search - - this.videoLink = [ '/search/lazy-load-video', { url: this.video.url, externalRedirect, fromPath } ] - return - } - - this.videoLink = [ '/videos/watch', this.video.uuid ] - } - - displayOwnerAccount () { - return this.ownerDisplayTypeChosen === 'account' - } - - displayOwnerVideoChannel () { - return this.ownerDisplayTypeChosen === 'videoChannel' - } - - isUnlistedVideo () { - return this.video.privacy.id === VideoPrivacy.UNLISTED - } - - isPrivateVideo () { - return this.video.privacy.id === VideoPrivacy.PRIVATE - } - - getStateLabel (video: Video) { - if (!video.state) return '' - - if (video.privacy.id !== VideoPrivacy.PRIVATE && video.state.id === VideoState.PUBLISHED) { - return this.i18n('Published') - } - - if (video.scheduledUpdate) { - const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId) - return this.i18n('Publication scheduled on ') + updateAt - } - - if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) { - return this.i18n('Waiting transcoding') - } - - if (video.state.id === VideoState.TO_TRANSCODE) { - return this.i18n('To transcode') - } - - if (video.state.id === VideoState.TO_IMPORT) { - return this.i18n('To import') - } - - return '' - } - - getAvatarUrl () { - if (this.ownerDisplayTypeChosen === 'account') { - return this.video.accountAvatarUrl - } - - return this.video.videoChannelAvatarUrl - } - - loadActions () { - if (this.displayVideoActions) this.showActions = true - - this.loadWatchLater() - } - - onVideoBlocked () { - this.videoBlocked.emit() - } - - onVideoUnblocked () { - this.videoUnblocked.emit() - } - - onVideoRemoved () { - this.videoRemoved.emit() - } - - isUserLoggedIn () { - return this.authService.isLoggedIn() - } - - onWatchLaterClick (currentState: boolean) { - if (currentState === true) this.removeFromWatchLater() - else this.addToWatchLater() - - this.inWatchLaterPlaylist = !currentState - } - - addToWatchLater () { - const body = { videoId: this.video.id } - - this.videoPlaylistService.addVideoInPlaylist(this.watchLaterPlaylist.id, body).subscribe( - res => { - this.watchLaterPlaylist.playlistElementId = res.videoPlaylistElement.id - } - ) - } - - removeFromWatchLater () { - this.videoPlaylistService.removeVideoFromPlaylist(this.watchLaterPlaylist.id, this.watchLaterPlaylist.playlistElementId, this.video.id) - .subscribe( - _ => { /* empty */ } - ) - } - - isWatchLaterPlaylistDisplayed () { - return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined - } - - private setUpBy () { - if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { - this.ownerDisplayTypeChosen = this.ownerDisplayType - return - } - - // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) - // -> Use the account name - if ( - this.video.channel.name === `${this.video.account.name}_channel` || - this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) - ) { - this.ownerDisplayTypeChosen = 'account' - } else { - this.ownerDisplayTypeChosen = 'videoChannel' - } - } - - private loadWatchLater () { - if (!this.isUserLoggedIn() || this.inWatchLaterPlaylist !== undefined) return - - this.authService.userInformationLoaded - .pipe(switchMap(() => this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id))) - .subscribe(existResult => { - const watchLaterPlaylist = this.authService.getUser().specialPlaylists.find(p => p.type === VideoPlaylistType.WATCH_LATER) - const existsInWatchLater = existResult.find(r => r.playlistId === watchLaterPlaylist.id) - this.inWatchLaterPlaylist = false - - this.watchLaterPlaylist = { - id: watchLaterPlaylist.id - } - - if (existsInWatchLater) { - this.inWatchLaterPlaylist = true - this.watchLaterPlaylist.playlistElementId = existsInWatchLater.playlistElementId - } - - this.cd.markForCheck() - }) - - this.videoPlaylistService.runPlaylistCheck(this.video.id) - } -} diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html deleted file mode 100644 index fe5510c56..000000000 --- a/client/src/app/shared/video/video-thumbnail.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - -
- -
- -
-
- - -
- -
-
-
- -
-
- -
{{ video.durationLabel }}
- -
-
-
- -
-
-
-
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss deleted file mode 100644 index feff78a87..000000000 --- a/client/src/app/shared/video/video-thumbnail.component.scss +++ /dev/null @@ -1,74 +0,0 @@ -@import '_variables'; -@import '_mixins'; -@import '_miniature'; - -.video-thumbnail { - @include miniature-thumbnail; - - .progress-bar { - height: 3px; - width: 100%; - position: absolute; - bottom: 0; - background-color: rgba(0, 0, 0, 0.20); - - div { - height: 100%; - background-color: pvar(--mainColor); - } - } - - .video-thumbnail-watch-later-overlay, - .video-thumbnail-label-overlay, - .video-thumbnail-duration-overlay { - @include static-thumbnail-overlay; - - border-radius: 3px; - font-size: 12px; - font-weight: $font-semibold; - line-height: 1.2; - z-index: z(miniature); - } - - .video-thumbnail-label-overlay { - position: absolute; - padding: 0 5px; - left: 5px; - top: 5px; - font-weight: $font-bold; - - &.warning { background-color: orange; } - &.danger { background-color: red; } - } - - .video-thumbnail-duration-overlay { - position: absolute; - padding: 0 3px; - right: 5px; - bottom: 5px; - } - - .video-thumbnail-actions-overlay { - position: absolute; - display: flex; - flex-direction: column; - right: 5px; - top: 5px; - opacity: 0; - - div:not(:first-child) { - margin-top: 2px; - } - - .video-thumbnail-watch-later-overlay { - padding: 3px; - - my-global-icon { - width: 22px; - height: 22px; - - @include apply-svg-color(#fff); - } - } - } -} diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts deleted file mode 100644 index 111b4c8bb..000000000 --- a/client/src/app/shared/video/video-thumbnail.component.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core' -import { Video } from './video.model' -import { ScreenService } from '@app/shared/misc/screen.service' -import { I18n } from '@ngx-translate/i18n-polyfill' - -@Component({ - selector: 'my-video-thumbnail', - styleUrls: [ './video-thumbnail.component.scss' ], - templateUrl: './video-thumbnail.component.html' -}) -export class VideoThumbnailComponent { - @Input() video: Video - @Input() nsfw = false - @Input() routerLink: any[] - @Input() queryParams: { [ p: string ]: any } - - @Input() displayWatchLaterPlaylist: boolean - @Input() inWatchLaterPlaylist: boolean - - @Output() watchLaterClick = new EventEmitter() - - addToWatchLaterText: string - addedToWatchLaterText: string - - constructor ( - private screenService: ScreenService, - private i18n: I18n - ) { - this.addToWatchLaterText = this.i18n('Add to watch later') - this.addedToWatchLaterText = this.i18n('Remove from watch later') - } - - getImageUrl () { - if (!this.video) return '' - - if (this.screenService.isInMobileView()) { - return this.video.previewUrl - } - - return this.video.thumbnailUrl - } - - getProgressPercent () { - if (!this.video.userHistory) return 0 - - const currentTime = this.video.userHistory.currentTime - - return (currentTime / this.video.duration) * 100 - } - - getVideoRouterLink () { - if (this.routerLink) return this.routerLink - - return [ '/videos/watch', this.video.uuid ] - } - - onWatchLaterClick (event: Event) { - this.watchLaterClick.emit(this.inWatchLaterPlaylist) - - event.stopPropagation() - return false - } -} diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts deleted file mode 100644 index dc5f45626..000000000 --- a/client/src/app/shared/video/video.model.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { User } from '../' -import { UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' -import { Avatar } from '../../../../../shared/models/avatars/avatar.model' -import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' -import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' -import { peertubeTranslate, ServerConfig } from '../../../../../shared/models' -import { Actor } from '@app/shared/actor/actor.model' -import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' -import { AuthUser } from '@app/core' -import { environment } from '../../../environments/environment' - -export class Video implements VideoServerModel { - byVideoChannel: string - byAccount: string - - accountAvatarUrl: string - videoChannelAvatarUrl: string - - createdAt: Date - updatedAt: Date - publishedAt: Date - originallyPublishedAt: Date | string - category: VideoConstant - licence: VideoConstant - language: VideoConstant - privacy: VideoConstant - description: string - duration: number - durationLabel: string - id: number - uuid: string - isLocal: boolean - name: string - serverHost: string - thumbnailPath: string - thumbnailUrl: string - - previewPath: string - previewUrl: string - - embedPath: string - embedUrl: string - - url?: string - - views: number - likes: number - dislikes: number - nsfw: boolean - - originInstanceUrl: string - originInstanceHost: string - - waitTranscoding?: boolean - state?: VideoConstant - scheduledUpdate?: VideoScheduleUpdate - blacklisted?: boolean - blockedReason?: string - - account: { - id: number - name: string - displayName: string - url: string - host: string - avatar?: Avatar - } - - channel: { - id: number - name: string - displayName: string - url: string - host: string - avatar?: Avatar - } - - userHistory?: { - currentTime: number - } - - static buildClientUrl (videoUUID: string) { - return '/videos/watch/' + videoUUID - } - - constructor (hash: VideoServerModel, translations = {}) { - const absoluteAPIUrl = getAbsoluteAPIUrl() - - this.createdAt = new Date(hash.createdAt.toString()) - this.publishedAt = new Date(hash.publishedAt.toString()) - this.category = hash.category - this.licence = hash.licence - this.language = hash.language - this.privacy = hash.privacy - this.waitTranscoding = hash.waitTranscoding - this.state = hash.state - this.description = hash.description - - this.duration = hash.duration - this.durationLabel = durationToString(hash.duration) - - this.id = hash.id - this.uuid = hash.uuid - - this.isLocal = hash.isLocal - this.name = hash.name - - this.thumbnailPath = hash.thumbnailPath - this.thumbnailUrl = hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath) - - this.previewPath = hash.previewPath - this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath) - - this.embedPath = hash.embedPath - this.embedUrl = hash.embedUrl || (environment.embedUrl + hash.embedPath) - - this.url = hash.url - - this.views = hash.views - this.likes = hash.likes - this.dislikes = hash.dislikes - - this.nsfw = hash.nsfw - - this.account = hash.account - this.channel = hash.channel - - this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) - this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host) - this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) - this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel) - - this.category.label = peertubeTranslate(this.category.label, translations) - this.licence.label = peertubeTranslate(this.licence.label, translations) - this.language.label = peertubeTranslate(this.language.label, translations) - this.privacy.label = peertubeTranslate(this.privacy.label, translations) - - this.scheduledUpdate = hash.scheduledUpdate - this.originallyPublishedAt = hash.originallyPublishedAt ? new Date(hash.originallyPublishedAt.toString()) : null - - if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) - - this.blacklisted = hash.blacklisted - this.blockedReason = hash.blacklistedReason - - this.userHistory = hash.userHistory - - this.originInstanceHost = this.account.host - this.originInstanceUrl = 'https://' + this.originInstanceHost - } - - isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { - // Video is not NSFW, skip - if (this.nsfw === false) return false - - // Return user setting if logged in - if (user) return user.nsfwPolicy !== 'display' - - // Return default instance config - return serverConfig.instance.defaultNSFWPolicy !== 'display' - } - - isRemovableBy (user: AuthUser) { - return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) - } - - isBlockableBy (user: AuthUser) { - return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true - } - - isUnblockableBy (user: AuthUser) { - return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true - } - - isUpdatableBy (user: AuthUser) { - return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) - } - - canBeDuplicatedBy (user: AuthUser) { - return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) - } -} diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts deleted file mode 100644 index d66a1f809..000000000 --- a/client/src/app/shared/video/video.service.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { catchError, map, switchMap } from 'rxjs/operators' -import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { Observable } from 'rxjs' -import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared' -import { ResultList } from '../../../../../shared/models/result-list.model' -import { - UserVideoRate, - UserVideoRateType, - UserVideoRateUpdate, - VideoConstant, - VideoFilter, - VideoPrivacy, - VideoUpdate -} from '../../../../../shared/models/videos' -import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' -import { environment } from '../../../environments/environment' -import { ComponentPaginationLight } from '../rest/component-pagination.model' -import { RestExtractor } from '../rest/rest-extractor.service' -import { RestService } from '../rest/rest.service' -import { UserService } from '../users/user.service' -import { VideoSortField } from './sort-field.type' -import { VideoDetails } from './video-details.model' -import { VideoEdit } from './video-edit.model' -import { Video } from './video.model' -import { objectToFormData } from '@app/shared/misc/utils' -import { Account } from '@app/shared/account/account.model' -import { AccountService } from '@app/shared/account/account.service' -import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' -import { ServerService, AuthService } from '@app/core' -import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' -import { VideoChannel } from '@app/shared/video-channel/video-channel.model' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' -import { FfprobeData } from 'fluent-ffmpeg' - -export interface VideosProvider { - getVideos (parameters: { - videoPagination: ComponentPaginationLight, - sort: VideoSortField, - filter?: VideoFilter, - categoryOneOf?: number[], - languageOneOf?: string[] - nsfwPolicy: NSFWPolicyType - }): Observable> -} - -@Injectable() -export class VideoService implements VideosProvider { - static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' - static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' - - constructor ( - private authHttp: HttpClient, - private authService: AuthService, - private userService: UserService, - private restExtractor: RestExtractor, - private restService: RestService, - private serverService: ServerService, - private i18n: I18n - ) {} - - getVideoViewUrl (uuid: string) { - return VideoService.BASE_VIDEO_URL + uuid + '/views' - } - - getUserWatchingVideoUrl (uuid: string) { - return VideoService.BASE_VIDEO_URL + uuid + '/watching' - } - - getVideo (options: { videoId: string }): Observable { - return this.serverService.getServerLocale() - .pipe( - switchMap(translations => { - return this.authHttp.get(VideoService.BASE_VIDEO_URL + options.videoId) - .pipe(map(videoHash => ({ videoHash, translations }))) - }), - map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - updateVideo (video: VideoEdit) { - const language = video.language || null - const licence = video.licence || null - const category = video.category || null - const description = video.description || null - const support = video.support || null - const scheduleUpdate = video.scheduleUpdate || null - const originallyPublishedAt = video.originallyPublishedAt || null - - const body: VideoUpdate = { - name: video.name, - category, - licence, - language, - support, - description, - channelId: video.channelId, - privacy: video.privacy, - tags: video.tags, - nsfw: video.nsfw, - waitTranscoding: video.waitTranscoding, - commentsEnabled: video.commentsEnabled, - downloadEnabled: video.downloadEnabled, - thumbnailfile: video.thumbnailfile, - previewfile: video.previewfile, - scheduleUpdate, - originallyPublishedAt - } - - const data = objectToFormData(body) - - return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, data) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - uploadVideo (video: FormData) { - const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) - - return this.authHttp - .request<{ video: { id: number, uuid: string } }>(req) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable> { - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - params = this.restService.addObjectParams(params, { search }) - - return this.authHttp - .get>(UserService.BASE_USERS_URL + 'me/videos', { params }) - .pipe( - switchMap(res => this.extractVideos(res)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getAccountVideos ( - account: Account, - videoPagination: ComponentPaginationLight, - sort: VideoSortField - ): Observable> { - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - return this.authHttp - .get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) - .pipe( - switchMap(res => this.extractVideos(res)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getVideoChannelVideos ( - videoChannel: VideoChannel, - videoPagination: ComponentPaginationLight, - sort: VideoSortField, - nsfwPolicy?: NSFWPolicyType - ): Observable> { - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (nsfwPolicy) { - params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) - } - - return this.authHttp - .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) - .pipe( - switchMap(res => this.extractVideos(res)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getUserSubscriptionVideos (parameters: { - videoPagination: ComponentPaginationLight, - sort: VideoSortField, - skipCount?: boolean - }): Observable> { - const { videoPagination, sort, skipCount } = parameters - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (skipCount) params = params.set('skipCount', skipCount + '') - - return this.authHttp - .get>(UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/videos', { params }) - .pipe( - switchMap(res => this.extractVideos(res)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getVideos (parameters: { - videoPagination: ComponentPaginationLight, - sort: VideoSortField, - filter?: VideoFilter, - categoryOneOf?: number[], - languageOneOf?: string[], - skipCount?: boolean, - nsfwPolicy?: NSFWPolicyType - }): Observable> { - const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy } = parameters - - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (filter) params = params.set('filter', filter) - if (skipCount) params = params.set('skipCount', skipCount + '') - - if (nsfwPolicy) { - params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) - } - - if (languageOneOf) { - for (const l of languageOneOf) { - params = params.append('languageOneOf[]', l) - } - } - - if (categoryOneOf) { - for (const c of categoryOneOf) { - params = params.append('categoryOneOf[]', c + '') - } - } - - return this.authHttp - .get>(VideoService.BASE_VIDEO_URL, { params }) - .pipe( - switchMap(res => this.extractVideos(res)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - buildBaseFeedUrls (params: HttpParams) { - const feeds = [ - { - format: FeedFormat.RSS, - label: 'rss 2.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase() - }, - { - format: FeedFormat.ATOM, - label: 'atom 1.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase() - }, - { - format: FeedFormat.JSON, - label: 'json 1.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() - } - ] - - if (params && params.keys().length !== 0) { - for (const feed of feeds) { - feed.url += '?' + params.toString() - } - } - - return feeds - } - - getVideoFeedUrls (sort: VideoSortField, filter?: VideoFilter, categoryOneOf?: number[]) { - let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort) - - if (filter) params = params.set('filter', filter) - - if (categoryOneOf) { - for (const c of categoryOneOf) { - params = params.append('categoryOneOf[]', c + '') - } - } - - return this.buildBaseFeedUrls(params) - } - - getAccountFeedUrls (accountId: number) { - let params = this.restService.addRestGetParams(new HttpParams()) - params = params.set('accountId', accountId.toString()) - - return this.buildBaseFeedUrls(params) - } - - getVideoChannelFeedUrls (videoChannelId: number) { - let params = this.restService.addRestGetParams(new HttpParams()) - params = params.set('videoChannelId', videoChannelId.toString()) - - return this.buildBaseFeedUrls(params) - } - - getVideoFileMetadata (metadataUrl: string) { - return this.authHttp - .get(metadataUrl) - .pipe( - catchError(err => this.restExtractor.handleError(err)) - ) - } - - removeVideo (id: number) { - return this.authHttp - .delete(VideoService.BASE_VIDEO_URL + id) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - loadCompleteDescription (descriptionPath: string) { - return this.authHttp - .get<{ description: string }>(environment.apiUrl + descriptionPath) - .pipe( - map(res => res.description), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - setVideoLike (id: number) { - return this.setVideoRate(id, 'like') - } - - setVideoDislike (id: number) { - return this.setVideoRate(id, 'dislike') - } - - unsetVideoLike (id: number) { - return this.setVideoRate(id, 'none') - } - - getUserVideoRating (id: number) { - const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' - - return this.authHttp.get(url) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - extractVideos (result: ResultList) { - return this.serverService.getServerLocale() - .pipe( - map(translations => { - const videosJson = result.data - const totalVideos = result.total - const videos: Video[] = [] - - for (const videoJson of videosJson) { - videos.push(new Video(videoJson, translations)) - } - - return { total: totalVideos, data: videos } - }) - ) - } - - explainedPrivacyLabels (privacies: VideoConstant[]) { - const base = [ - { - id: VideoPrivacy.PRIVATE, - label: this.i18n('Only I can see this video') - }, - { - id: VideoPrivacy.UNLISTED, - label: this.i18n('Only people with the private link can see this video') - }, - { - id: VideoPrivacy.PUBLIC, - label: this.i18n('Anyone can see this video') - }, - { - id: VideoPrivacy.INTERNAL, - label: this.i18n('Only users of this instance can see this video') - } - ] - - return base.filter(o => !!privacies.find(p => p.id === o.id)) - } - - nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { - return nsfwPolicy === 'do_not_list' - ? 'false' - : 'both' - } - - private setVideoRate (id: number, rateType: UserVideoRateType) { - const url = VideoService.BASE_VIDEO_URL + id + '/rate' - const body: UserVideoRateUpdate = { - rating: rateType - } - - return this.authHttp - .put(url, body) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) - } -} diff --git a/client/src/app/shared/video/videos-selection.component.html b/client/src/app/shared/video/videos-selection.component.html deleted file mode 100644 index 44aa567b9..000000000 --- a/client/src/app/shared/video/videos-selection.component.html +++ /dev/null @@ -1,30 +0,0 @@ -
No results.
- -
-
- -
- -
- - - - -
-
- - Cancel - - - -
-
- - - - -
-
diff --git a/client/src/app/shared/video/videos-selection.component.scss b/client/src/app/shared/video/videos-selection.component.scss deleted file mode 100644 index d3cbabf23..000000000 --- a/client/src/app/shared/video/videos-selection.component.scss +++ /dev/null @@ -1,57 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.action-selection-mode { - display: flex; - justify-content: flex-end; - flex-grow: 1; - - .action-selection-mode-child { - position: fixed; - - .action-button { - display: inline-block; - } - - .action-button-cancel-selection { - @include peertube-button; - @include grey-button; - - margin-right: 10px; - } - } -} - -.video { - @include row-blocks; - - &:first-child { - margin-top: 47px; - } - - .checkbox-container { - display: flex; - align-items: center; - margin-right: 20px; - margin-left: 12px; - } - - my-video-miniature { - flex-grow: 1; - } -} - -@media screen and (max-width: $small-view) { - .video { - flex-direction: column; - height: auto; - - .checkbox-container { - display: none; - } - - my-button { - margin-top: 10px; - } - } -} diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts deleted file mode 100644 index 9453664dd..000000000 --- a/client/src/app/shared/video/videos-selection.component.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - AfterContentInit, - Component, - ContentChildren, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, - QueryList, - TemplateRef -} from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AbstractVideoList } from '@app/shared/video/abstract-video-list' -import { AuthService, Notifier, ServerService } from '@app/core' -import { ScreenService } from '@app/shared/misc/screen.service' -import { MiniatureDisplayOptions, OwnerDisplayType } from '@app/shared/video/video-miniature.component' -import { Observable } from 'rxjs' -import { Video } from '@app/shared/video/video.model' -import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' -import { VideoSortField } from '@app/shared/video/sort-field.type' -import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { ResultList } from '@shared/models' -import { UserService } from '../users' -import { LocalStorageService } from '../misc/storage.service' - -export type SelectionType = { [ id: number ]: boolean } - -@Component({ - selector: 'my-videos-selection', - templateUrl: './videos-selection.component.html', - styleUrls: [ './videos-selection.component.scss' ] -}) -export class VideosSelectionComponent extends AbstractVideoList implements OnInit, OnDestroy, AfterContentInit { - @Input() pagination: ComponentPagination - @Input() titlePage: string - @Input() miniatureDisplayOptions: MiniatureDisplayOptions - @Input() ownerDisplayType: OwnerDisplayType - - @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable> - - @ContentChildren(PeerTubeTemplateDirective) templates: QueryList> - - @Output() selectionChange = new EventEmitter() - @Output() videosModelChange = new EventEmitter() - - _selection: SelectionType = {} - - rowButtonsTemplate: TemplateRef - globalButtonsTemplate: TemplateRef - - constructor ( - protected i18n: I18n, - protected router: Router, - protected route: ActivatedRoute, - protected notifier: Notifier, - protected authService: AuthService, - protected userService: UserService, - protected screenService: ScreenService, - protected storageService: LocalStorageService, - protected serverService: ServerService - ) { - super() - } - - @Input() get selection () { - return this._selection - } - - set selection (selection: SelectionType) { - this._selection = selection - this.selectionChange.emit(this._selection) - } - - @Input() get videosModel () { - return this.videos - } - - set videosModel (videos: Video[]) { - this.videos = videos - this.videosModelChange.emit(this.videos) - } - - ngOnInit () { - super.ngOnInit() - } - - ngAfterContentInit () { - { - const t = this.templates.find(t => t.name === 'rowButtons') - if (t) this.rowButtonsTemplate = t.template - } - - { - const t = this.templates.find(t => t.name === 'globalButtons') - if (t) this.globalButtonsTemplate = t.template - } - } - - ngOnDestroy () { - super.ngOnDestroy() - } - - getVideosObservable (page: number) { - return this.getVideosObservableFunction(page, this.sort) - } - - abortSelectionMode () { - this._selection = {} - } - - isInSelectionMode () { - return Object.keys(this._selection).some(k => this._selection[ k ] === true) - } - - generateSyndicationList () { - throw new Error('Method not implemented.') - } - - protected onMoreVideos () { - this.videosModel = this.videos - } -} -- cgit v1.2.3