From 693263e936763a851e3c8c020e3739def8bd4eca Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 4 Apr 2019 10:44:18 +0200 Subject: Refactor videos selection components --- .../video-auto-blacklist-list.component.html | 48 ++++----- .../video-auto-blacklist-list.component.scss | 67 ++---------- .../video-auto-blacklist-list.component.ts | 56 ++++------- .../my-account-videos.component.html | 63 +++++------- .../my-account-videos.component.scss | 76 ++------------ .../my-account-videos.component.ts | 72 ++++--------- client/src/app/shared/angular/from-now.pipe.ts | 40 ++++++++ .../app/shared/angular/number-formatter.pipe.ts | 19 ++++ .../src/app/shared/angular/object-length.pipe.ts | 8 ++ .../shared/angular/peertube-template.directive.ts | 12 +++ client/src/app/shared/misc/from-now.pipe.ts | 40 -------- .../src/app/shared/misc/number-formatter.pipe.ts | 19 ---- client/src/app/shared/misc/object-length.pipe.ts | 8 -- client/src/app/shared/shared.module.ts | 17 +++- client/src/app/shared/video/abstract-video-list.ts | 5 + .../shared/video/videos-selection.component.html | 26 +++++ .../shared/video/videos-selection.component.scss | 57 +++++++++++ .../app/shared/video/videos-selection.component.ts | 112 +++++++++++++++++++++ 18 files changed, 390 insertions(+), 355 deletions(-) create mode 100644 client/src/app/shared/angular/from-now.pipe.ts create mode 100644 client/src/app/shared/angular/number-formatter.pipe.ts create mode 100644 client/src/app/shared/angular/object-length.pipe.ts create mode 100644 client/src/app/shared/angular/peertube-template.directive.ts delete mode 100644 client/src/app/shared/misc/from-now.pipe.ts delete mode 100644 client/src/app/shared/misc/number-formatter.pipe.ts delete mode 100644 client/src/app/shared/misc/object-length.pipe.ts create mode 100644 client/src/app/shared/video/videos-selection.component.html create mode 100644 client/src/app/shared/video/videos-selection.component.scss create mode 100644 client/src/app/shared/video/videos-selection.component.ts diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html index 5ef497fa7..62dde60bb 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html @@ -1,33 +1,19 @@ -
No results.
+ + + + + Unblacklist + + -
-
-
- -
+ + + - - - -
-
- - Cancel - - - - - Unblacklist - -
-
- - -
-
+
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss index e43a2aa7b..85ebc6041 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss @@ -1,67 +1,14 @@ @import '_variables'; @import '_mixins'; -.action-selection-mode { - width: 194px; - display: flex; - justify-content: flex-end; +.action-button-unblacklist-selection { + display: inline-block; - .action-selection-mode-child { - position: fixed; + @include peertube-button; + @include orange-button; + @include button-with-icon(21px); - .action-button { - display: inline-block; - } - - .action-button-cancel-selection { - @include peertube-button; - @include grey-button; - - margin-right: 10px; - } - - .action-button-unblacklist-selection { - @include peertube-button; - @include orange-button; - @include button-with-icon(21px); - - my-global-icon { - @include apply-svg-color(#fff); - } - } - } -} - -.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; - } + my-global-icon { + @include apply-svg-color(#fff); } } diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts index d66a6dcae..fb2962b47 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts @@ -1,29 +1,23 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' +import { Component } from '@angular/core' import { I18n } from '@ngx-translate/i18n-polyfill' import { ActivatedRoute, Router } from '@angular/router' -import { AbstractVideoList } from '@app/shared/video/abstract-video-list' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { AuthService, Notifier, ServerService } from '@app/core' -import { Video } from '@shared/models' import { VideoBlacklistService } from '@app/shared' import { immutableAssign } from '@app/shared/misc/utils' import { ScreenService } from '@app/shared/misc/screen.service' import { MiniatureDisplayOptions } from '@app/shared/video/video-miniature.component' +import { SelectionType } from '@app/shared/video/videos-selection.component' +import { Video } from '@app/shared/video/video.model' @Component({ selector: 'my-video-auto-blacklist-list', templateUrl: './video-auto-blacklist-list.component.html', styleUrls: [ './video-auto-blacklist-list.component.scss' ] }) -export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy { +export class VideoAutoBlacklistListComponent { titlePage: string - checkedVideos: { [ id: number ]: boolean } = {} - pagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 5, - totalItems: null - } - + selection: SelectionType = {} miniatureDisplayOptions: MiniatureDisplayOptions = { date: true, views: false, @@ -34,6 +28,13 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement blacklistInfo: false, nsfw: true } + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 5, + totalItems: null + } + videos: Video[] = [] + getVideosObservableFunction = this.getVideosObservable.bind(this) constructor ( protected router: Router, @@ -45,42 +46,21 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement private i18n: I18n, private videoBlacklistService: VideoBlacklistService ) { - super() - this.titlePage = this.i18n('Auto-blacklisted videos') } - ngOnInit () { - super.ngOnInit() - } - - ngOnDestroy () { - super.ngOnDestroy() - } - - abortSelectionMode () { - this.checkedVideos = {} - } - - isInSelectionMode () { - return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) - } - getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) return this.videoBlacklistService.getAutoBlacklistedAsVideoList(newPagination) } - generateSyndicationList () { - throw new Error('Method not implemented.') - } - removeVideoFromBlacklist (entry: Video) { this.videoBlacklistService.removeVideoFromBlacklist(entry.id).subscribe( () => { this.notifier.success(this.i18n('Video {{name}} removed from blacklist.', { name: entry.name })) - this.reloadVideos() + + this.videos = this.videos.filter(v => v.id !== entry.id) }, error => this.notifier.error(error.message) @@ -88,16 +68,16 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement } removeSelectedVideosFromBlacklist () { - const toReleaseVideosIds = Object.keys(this.checkedVideos) - .filter(k => this.checkedVideos[ k ] === true) + const toReleaseVideosIds = Object.keys(this.selection) + .filter(k => this.selection[ k ] === true) .map(k => parseInt(k, 10)) this.videoBlacklistService.removeVideoFromBlacklist(toReleaseVideosIds).subscribe( () => { this.notifier.success(this.i18n('{{num}} videos removed from blacklist.', { num: toReleaseVideosIds.length })) - this.abortSelectionMode() - this.reloadVideos() + this.selection = {} + this.videos = this.videos.filter(v => toReleaseVideosIds.includes(v.id) === false) }, error => this.notifier.error(error.message) diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index 3a4054de8..d7993fdc2 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html @@ -1,39 +1,30 @@ -
No results.
+ + + + + Delete + + + + + + + + + + + -
-
-
- -
- - - - -
-
- - Cancel - - - - - Delete - -
-
- -
- - - - - -
-
-
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index 405ded3f8..87398e7c8 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss @@ -1,75 +1,19 @@ @import '_variables'; @import '_mixins'; -.action-selection-mode { - width: 174px; - display: flex; - justify-content: flex-end; +.action-button-delete-selection { + display: inline-block; - .action-selection-mode-child { - position: fixed; + @include peertube-button; + @include orange-button; + @include button-with-icon(21px); - .action-button { - display: inline-block; - } - - .action-button-cancel-selection { - @include peertube-button; - @include grey-button; - - margin-right: 10px; - } - - .action-button-delete-selection { - @include peertube-button; - @include orange-button; - @include button-with-icon(21px); - - my-global-icon { - @include apply-svg-color(#fff); - } - } - } -} - -.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; - } - - .video-buttons { - min-width: 190px; - - *:not(:last-child) { - margin-right: 10px; - } + my-global-icon { + @include apply-svg-color(#fff); } } -@media screen and (max-width: $small-view) { - .video { - flex-direction: column; - height: auto; - - .checkbox-container { - display: none; - } - - .video-buttons { - margin-top: 10px; - } - } +my-delete-button, +my-edit-button { + margin-right: 10px; } diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index bbe86af73..5f29364a8 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts @@ -1,31 +1,33 @@ import { concat, Observable } from 'rxjs' import { tap, toArray } from 'rxjs/operators' -import { Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { Component, ViewChild } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { immutableAssign } from '@app/shared/misc/utils' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { Notifier, ServerService } from '@app/core' import { AuthService } from '../../core/auth' import { ConfirmService } from '../../core/confirm' -import { AbstractVideoList } from '../../shared/video/abstract-video-list' import { Video } from '../../shared/video/video.model' import { VideoService } from '../../shared/video/video.service' import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoPrivacy, VideoState } from '../../../../../shared/models/videos' import { ScreenService } from '@app/shared/misc/screen.service' import { VideoChangeOwnershipComponent } from './video-change-ownership/video-change-ownership.component' import { MiniatureDisplayOptions } from '@app/shared/video/video-miniature.component' +import { SelectionType, VideosSelectionComponent } from '@app/shared/video/videos-selection.component' +import { VideoSortField } from '@app/shared/video/sort-field.type' +import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' @Component({ selector: 'my-account-videos', templateUrl: './my-account-videos.component.html', styleUrls: [ './my-account-videos.component.scss' ] }) -export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { +export class MyAccountVideosComponent implements DisableForReuseHook { + @ViewChild('videosSelection') videosSelection: VideosSelectionComponent @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent titlePage: string - checkedVideos: { [ id: number ]: boolean } = {} + selection: SelectionType = {} pagination: ComponentPagination = { currentPage: 1, itemsPerPage: 5, @@ -40,6 +42,8 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni state: true, blacklistInfo: true } + videos: Video[] = [] + getVideosObservableFunction = this.getVideosObservable.bind(this) constructor ( protected router: Router, @@ -50,43 +54,28 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni protected screenService: ScreenService, private i18n: I18n, private confirmService: ConfirmService, - private videoService: VideoService, - @Inject(LOCALE_ID) private localeId: string + private videoService: VideoService ) { - super() - this.titlePage = this.i18n('My videos') } - ngOnInit () { - super.ngOnInit() - } - - ngOnDestroy () { - super.ngOnDestroy() + disableForReuse () { + this.videosSelection.disableForReuse() } - abortSelectionMode () { - this.checkedVideos = {} + enabledForReuse () { + this.videosSelection.enabledForReuse() } - isInSelectionMode () { - return Object.keys(this.checkedVideos).some(k => this.checkedVideos[ k ] === true) - } - - getVideosObservable (page: number) { + getVideosObservable (page: number, sort: VideoSortField) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) - return this.videoService.getMyVideos(newPagination, this.sort) - } - - generateSyndicationList () { - throw new Error('Method not implemented.') + return this.videoService.getMyVideos(newPagination, sort) } async deleteSelectedVideos () { - const toDeleteVideosIds = Object.keys(this.checkedVideos) - .filter(k => this.checkedVideos[ k ] === true) + const toDeleteVideosIds = Object.keys(this.selection) + .filter(k => this.selection[ k ] === true) .map(k => parseInt(k, 10)) const res = await this.confirmService.confirm( @@ -109,7 +98,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni () => { this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })) - this.abortSelectionMode() + this.selection = {} }, err => this.notifier.error(err.message) @@ -127,7 +116,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni .subscribe( () => { this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: video.name })) - this.reloadVideos() + this.removeVideoFromArray(video.id) }, error => this.notifier.error(error.message) @@ -139,27 +128,6 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni this.videoChangeOwnershipModal.show(video) } - getStateLabel (video: Video) { - let suffix: string - - if (video.privacy.id !== VideoPrivacy.PRIVATE && video.state.id === VideoState.PUBLISHED) { - suffix = this.i18n('Published') - } else if (video.scheduledUpdate) { - const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId) - suffix = this.i18n('Publication scheduled on ') + updateAt - } else if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) { - suffix = this.i18n('Waiting transcoding') - } else if (video.state.id === VideoState.TO_TRANSCODE) { - suffix = this.i18n('To transcode') - } else if (video.state.id === VideoState.TO_IMPORT) { - suffix = this.i18n('To import') - } else { - return '' - } - - return ' - ' + suffix - } - private removeVideoFromArray (id: number) { this.videos = this.videos.filter(v => v.id !== id) } diff --git a/client/src/app/shared/angular/from-now.pipe.ts b/client/src/app/shared/angular/from-now.pipe.ts new file mode 100644 index 000000000..3a9a76411 --- /dev/null +++ b/client/src/app/shared/angular/from-now.pipe.ts @@ -0,0 +1,40 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { I18n } from '@ngx-translate/i18n-polyfill' + +// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site +@Pipe({ name: 'myFromNow' }) +export class FromNowPipe implements PipeTransform { + + constructor (private i18n: I18n) { } + + transform (arg: number | Date | string) { + const argDate = new Date(arg) + const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) + + let interval = Math.floor(seconds / 31536000) + if (interval > 1) { + return this.i18n('{{interval}} years ago', { interval }) + } + + interval = Math.floor(seconds / 2592000) + if (interval > 1) return this.i18n('{{interval}} months ago', { interval }) + if (interval === 1) return this.i18n('{{interval}} month ago', { interval }) + + interval = Math.floor(seconds / 604800) + if (interval > 1) return this.i18n('{{interval}} weeks ago', { interval }) + if (interval === 1) return this.i18n('{{interval}} week ago', { interval }) + + interval = Math.floor(seconds / 86400) + if (interval > 1) return this.i18n('{{interval}} days ago', { interval }) + if (interval === 1) return this.i18n('{{interval}} day ago', { interval }) + + interval = Math.floor(seconds / 3600) + if (interval > 1) return this.i18n('{{interval}} hours ago', { interval }) + if (interval === 1) return this.i18n('{{interval}} hour ago', { interval }) + + interval = Math.floor(seconds / 60) + if (interval >= 1) return this.i18n('{{interval}} min ago', { interval }) + + return this.i18n('{{interval}} sec ago', { interval: Math.max(0, seconds) }) + } +} diff --git a/client/src/app/shared/angular/number-formatter.pipe.ts b/client/src/app/shared/angular/number-formatter.pipe.ts new file mode 100644 index 000000000..8a0756a36 --- /dev/null +++ b/client/src/app/shared/angular/number-formatter.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from '@angular/core' + +// Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts + +@Pipe({ name: 'myNumberFormatter' }) +export class NumberFormatterPipe implements PipeTransform { + private dictionary: Array<{max: number, type: string}> = [ + { max: 1000, type: '' }, + { max: 1000000, type: 'K' }, + { max: 1000000000, type: 'M' } + ] + + transform (value: number) { + const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1] + const calc = Math.floor(value / (format.max / 1000)) + + return `${calc}${format.type}` + } +} diff --git a/client/src/app/shared/angular/object-length.pipe.ts b/client/src/app/shared/angular/object-length.pipe.ts new file mode 100644 index 000000000..84d182052 --- /dev/null +++ b/client/src/app/shared/angular/object-length.pipe.ts @@ -0,0 +1,8 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ name: 'myObjectLength' }) +export class ObjectLengthPipe implements PipeTransform { + transform (value: Object) { + return Object.keys(value).length + } +} diff --git a/client/src/app/shared/angular/peertube-template.directive.ts b/client/src/app/shared/angular/peertube-template.directive.ts new file mode 100644 index 000000000..a514b6057 --- /dev/null +++ b/client/src/app/shared/angular/peertube-template.directive.ts @@ -0,0 +1,12 @@ +import { Directive, Input, TemplateRef } from '@angular/core' + +@Directive({ + selector: '[ptTemplate]' +}) +export class PeerTubeTemplateDirective { + @Input('ptTemplate') name: string + + constructor (public template: TemplateRef) { + // empty + } +} diff --git a/client/src/app/shared/misc/from-now.pipe.ts b/client/src/app/shared/misc/from-now.pipe.ts deleted file mode 100644 index 3a9a76411..000000000 --- a/client/src/app/shared/misc/from-now.pipe.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { I18n } from '@ngx-translate/i18n-polyfill' - -// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site -@Pipe({ name: 'myFromNow' }) -export class FromNowPipe implements PipeTransform { - - constructor (private i18n: I18n) { } - - transform (arg: number | Date | string) { - const argDate = new Date(arg) - const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) - - let interval = Math.floor(seconds / 31536000) - if (interval > 1) { - return this.i18n('{{interval}} years ago', { interval }) - } - - interval = Math.floor(seconds / 2592000) - if (interval > 1) return this.i18n('{{interval}} months ago', { interval }) - if (interval === 1) return this.i18n('{{interval}} month ago', { interval }) - - interval = Math.floor(seconds / 604800) - if (interval > 1) return this.i18n('{{interval}} weeks ago', { interval }) - if (interval === 1) return this.i18n('{{interval}} week ago', { interval }) - - interval = Math.floor(seconds / 86400) - if (interval > 1) return this.i18n('{{interval}} days ago', { interval }) - if (interval === 1) return this.i18n('{{interval}} day ago', { interval }) - - interval = Math.floor(seconds / 3600) - if (interval > 1) return this.i18n('{{interval}} hours ago', { interval }) - if (interval === 1) return this.i18n('{{interval}} hour ago', { interval }) - - interval = Math.floor(seconds / 60) - if (interval >= 1) return this.i18n('{{interval}} min ago', { interval }) - - return this.i18n('{{interval}} sec ago', { interval: Math.max(0, seconds) }) - } -} diff --git a/client/src/app/shared/misc/number-formatter.pipe.ts b/client/src/app/shared/misc/number-formatter.pipe.ts deleted file mode 100644 index 8a0756a36..000000000 --- a/client/src/app/shared/misc/number-formatter.pipe.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' - -// Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts - -@Pipe({ name: 'myNumberFormatter' }) -export class NumberFormatterPipe implements PipeTransform { - private dictionary: Array<{max: number, type: string}> = [ - { max: 1000, type: '' }, - { max: 1000000, type: 'K' }, - { max: 1000000000, type: 'M' } - ] - - transform (value: number) { - const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1] - const calc = Math.floor(value / (format.max / 1000)) - - return `${calc}${format.type}` - } -} diff --git a/client/src/app/shared/misc/object-length.pipe.ts b/client/src/app/shared/misc/object-length.pipe.ts deleted file mode 100644 index 84d182052..000000000 --- a/client/src/app/shared/misc/object-length.pipe.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' - -@Pipe({ name: 'myObjectLength' }) -export class ObjectLengthPipe implements PipeTransform { - transform (value: Object) { - return Object.keys(value).length - } -} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 3647fc786..68225b457 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -14,10 +14,7 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' import { ButtonComponent } from './buttons/button.component' import { DeleteButtonComponent } from './buttons/delete-button.component' import { EditButtonComponent } from './buttons/edit-button.component' -import { FromNowPipe } from './misc/from-now.pipe' import { LoaderComponent } from './misc/loader.component' -import { NumberFormatterPipe } from './misc/number-formatter.pipe' -import { ObjectLengthPipe } from './misc/object-length.pipe' import { RestExtractor, RestService } from './rest' import { UserService } from './users' import { VideoAbuseService } from './video-abuse' @@ -78,6 +75,11 @@ import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/vide import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.component' import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playlist/video-playlist-element-miniature.component' +import { VideosSelectionComponent } from '@app/shared/video/videos-selection.component' +import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe' +import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' +import { FromNowPipe } from '@app/shared/angular/from-now.pipe' +import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' @NgModule({ imports: [ @@ -107,6 +109,7 @@ import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playli VideoPlaylistMiniatureComponent, VideoAddToPlaylistComponent, VideoPlaylistElementMiniatureComponent, + VideosSelectionComponent, FeedComponent, @@ -114,10 +117,12 @@ import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playli DeleteButtonComponent, EditButtonComponent, - ActionDropdownComponent, NumberFormatterPipe, ObjectLengthPipe, FromNowPipe, + PeerTubeTemplateDirective, + + ActionDropdownComponent, MarkdownTextareaComponent, InfiniteScrollerDirective, TextareaAutoResizeDirective, @@ -166,6 +171,7 @@ import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playli VideoPlaylistMiniatureComponent, VideoAddToPlaylistComponent, VideoPlaylistElementMiniatureComponent, + VideosSelectionComponent, FeedComponent, @@ -197,7 +203,8 @@ import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playli NumberFormatterPipe, ObjectLengthPipe, - FromNowPipe + FromNowPipe, + PeerTubeTemplateDirective ], providers: [ diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 467f629ea..099650129 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -102,6 +102,8 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor ({ videos, totalVideos }) => { this.pagination.totalItems = totalVideos this.videos = this.videos.concat(videos) + + this.onMoreVideos() }, error => this.notifier.error(error.message) @@ -118,6 +120,9 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor throw new Error('toggleModerationDisplay is not implemented') } + // 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' ] diff --git a/client/src/app/shared/video/videos-selection.component.html b/client/src/app/shared/video/videos-selection.component.html new file mode 100644 index 000000000..6f3401b4b --- /dev/null +++ b/client/src/app/shared/video/videos-selection.component.html @@ -0,0 +1,26 @@ +
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 new file mode 100644 index 000000000..d3cbabf23 --- /dev/null +++ b/client/src/app/shared/video/videos-selection.component.scss @@ -0,0 +1,57 @@ +@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 new file mode 100644 index 000000000..b6bedafd8 --- /dev/null +++ b/client/src/app/shared/video/videos-selection.component.ts @@ -0,0 +1,112 @@ +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 } 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' + +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() titlePage: string + @Input() miniatureDisplayOptions: MiniatureDisplayOptions + @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<{ videos: Video[], totalVideos: number }> + @ContentChildren(PeerTubeTemplateDirective) templates: QueryList + + @Output() selectionChange = new EventEmitter() + @Output() videosModelChange = new EventEmitter() + + _selection: SelectionType = {} + + rowButtonsTemplate: TemplateRef + globalButtonsTemplate: TemplateRef + + constructor ( + protected router: Router, + protected route: ActivatedRoute, + protected notifier: Notifier, + protected authService: AuthService, + protected screenService: ScreenService, + protected serverService: ServerService + ) { + super() + } + + 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 + } + } + + @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() + } + + 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