From 17119e4a546522468878cf115558b17949ab50d0 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 12 Nov 2020 15:28:54 +0100 Subject: Reorganize left menu and account menu Add my-settings and my-library in left menu Move administration below my-library Split account menu: my-setting and my library --- .../my-account-video-channel-create.component.ts | 83 ------ .../my-account-video-channel-edit.component.html | 105 -------- .../my-account-video-channel-edit.component.scss | 67 ----- .../my-account-video-channel-edit.ts | 19 -- .../my-account-video-channel-update.component.ts | 135 ---------- .../my-account-video-channels-routing.module.ts | 41 --- .../my-account-video-channels.component.html | 49 ---- .../my-account-video-channels.component.scss | 125 --------- .../my-account-video-channels.component.ts | 172 ------------ .../my-account-video-channels.module.ts | 31 --- .../my-account-history.component.html | 28 -- .../my-account-history.component.scss | 59 ---- .../my-account-history.component.ts | 103 ------- .../my-account-accept-ownership.component.html | 36 --- .../my-account-accept-ownership.component.scss | 14 - .../my-account-accept-ownership.component.ts | 72 ----- .../my-account-ownership.component.html | 90 ------- .../my-account-ownership.component.scss | 71 ----- .../my-account-ownership.component.ts | 83 ------ .../app/+my-account/my-account-routing.module.ts | 97 ++----- .../my-account-subscriptions.component.html | 42 --- .../my-account-subscriptions.component.scss | 81 ------ .../my-account-subscriptions.component.ts | 75 ------ .../my-account-video-imports.component.html | 70 ----- .../my-account-video-imports.component.scss | 14 - .../my-account-video-imports.component.ts | 77 ------ .../my-account-video-playlist-create.component.ts | 92 ------- .../my-account-video-playlist-edit.component.html | 100 ------- .../my-account-video-playlist-edit.component.scss | 36 --- .../my-account-video-playlist-edit.ts | 13 - ...-account-video-playlist-elements.component.html | 51 ---- ...-account-video-playlist-elements.component.scss | 83 ------ ...my-account-video-playlist-elements.component.ts | 198 -------------- .../my-account-video-playlist-update.component.ts | 130 --------- .../my-account-video-playlists.component.html | 35 --- .../my-account-video-playlists.component.scss | 78 ------ .../my-account-video-playlists.component.ts | 102 ------- .../modals/video-change-ownership.component.html | 33 --- .../modals/video-change-ownership.component.scss | 10 - .../modals/video-change-ownership.component.ts | 69 ----- .../my-account-videos.component.html | 46 ---- .../my-account-videos.component.scss | 127 --------- .../my-account-videos.component.ts | 179 ------------- client/src/app/+my-account/my-account.component.ts | 84 +----- client/src/app/+my-account/my-account.module.ts | 37 +-- .../my-video-channel-create.component.ts | 78 ++++++ .../my-video-channel-edit.component.html | 105 ++++++++ .../my-video-channel-edit.component.scss | 67 +++++ .../+my-video-channels/my-video-channel-edit.ts | 22 ++ .../my-video-channel-update.component.ts | 135 ++++++++++ .../my-video-channels-routing.module.ts | 41 +++ .../my-video-channels.component.html | 49 ++++ .../my-video-channels.component.scss | 125 +++++++++ .../my-video-channels.component.ts | 171 ++++++++++++ .../+my-video-channels/my-video-channels.module.ts | 31 +++ client/src/app/+my-library/index.ts | 3 + .../my-history/my-history.component.html | 28 ++ .../my-history/my-history.component.scss | 59 ++++ .../+my-library/my-history/my-history.component.ts | 102 +++++++ .../app/+my-library/my-library-routing.module.ts | 134 ++++++++++ .../src/app/+my-library/my-library.component.html | 7 + .../src/app/+my-library/my-library.component.scss | 13 + client/src/app/+my-library/my-library.component.ts | 76 ++++++ client/src/app/+my-library/my-library.module.ts | 79 ++++++ .../my-accept-ownership.component.html | 36 +++ .../my-accept-ownership.component.scss | 14 + .../my-accept-ownership.component.ts | 72 +++++ .../my-ownership/my-ownership.component.html | 90 +++++++ .../my-ownership/my-ownership.component.scss | 71 +++++ .../my-ownership/my-ownership.component.ts | 81 ++++++ .../my-subscriptions.component.html | 42 +++ .../my-subscriptions.component.scss | 81 ++++++ .../my-subscriptions/my-subscriptions.component.ts | 74 ++++++ .../my-video-imports.component.html | 70 +++++ .../my-video-imports.component.scss | 14 + .../my-video-imports/my-video-imports.component.ts | 76 ++++++ .../my-video-playlist-create.component.ts | 91 +++++++ .../my-video-playlist-edit.component.html | 100 +++++++ .../my-video-playlist-edit.component.scss | 36 +++ .../my-video-playlists/my-video-playlist-edit.ts | 13 + .../my-video-playlist-elements.component.html | 51 ++++ .../my-video-playlist-elements.component.scss | 83 ++++++ .../my-video-playlist-elements.component.ts | 197 ++++++++++++++ .../my-video-playlist-update.component.ts | 129 +++++++++ .../my-video-playlists.component.html | 35 +++ .../my-video-playlists.component.scss | 78 ++++++ .../my-video-playlists.component.ts | 101 +++++++ .../modals/video-change-ownership.component.html | 33 +++ .../modals/video-change-ownership.component.scss | 10 + .../modals/video-change-ownership.component.ts | 69 +++++ .../+my-library/my-videos/my-videos.component.html | 58 ++++ .../+my-library/my-videos/my-videos.component.scss | 138 ++++++++++ .../+my-library/my-videos/my-videos.component.ts | 178 +++++++++++++ .../+video-channels/video-channels.component.html | 2 +- .../video-import-torrent.component.ts | 2 +- .../video-import-url.component.ts | 2 +- .../video-user-subscriptions.component.ts | 2 +- client/src/app/app-routing.module.ts | 4 + client/src/app/core/auth/auth.service.ts | 6 +- client/src/app/menu/menu.component.html | 179 +++++++------ client/src/app/menu/menu.component.scss | 296 +++++++++++++-------- client/src/app/menu/menu.component.ts | 42 +-- .../shared-main/users/user-notification.model.ts | 2 +- ...video-playlist-element-miniature.component.html | 2 +- .../video-playlist-miniature.component.ts | 2 +- client/src/sass/include/_variables.scss | 5 +- 106 files changed, 3790 insertions(+), 3544 deletions(-) delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts delete mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts delete mode 100644 client/src/app/+my-account/my-account-history/my-account-history.component.html delete mode 100644 client/src/app/+my-account/my-account-history/my-account-history.component.scss delete mode 100644 client/src/app/+my-account/my-account-history/my-account-history.component.ts delete mode 100644 client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html delete mode 100644 client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss delete mode 100644 client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts delete mode 100644 client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html delete mode 100644 client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss delete mode 100644 client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts delete mode 100644 client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html delete mode 100644 client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss delete mode 100644 client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts delete mode 100644 client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html delete mode 100644 client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss delete mode 100644 client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss delete mode 100644 client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts delete mode 100644 client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html delete mode 100644 client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss delete mode 100644 client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts delete mode 100644 client/src/app/+my-account/my-account-videos/my-account-videos.component.html delete mode 100644 client/src/app/+my-account/my-account-videos/my-account-videos.component.scss delete mode 100644 client/src/app/+my-account/my-account-videos/my-account-videos.component.ts create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channels.component.html create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts create mode 100644 client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts create mode 100644 client/src/app/+my-library/index.ts create mode 100644 client/src/app/+my-library/my-history/my-history.component.html create mode 100644 client/src/app/+my-library/my-history/my-history.component.scss create mode 100644 client/src/app/+my-library/my-history/my-history.component.ts create mode 100644 client/src/app/+my-library/my-library-routing.module.ts create mode 100644 client/src/app/+my-library/my-library.component.html create mode 100644 client/src/app/+my-library/my-library.component.scss create mode 100644 client/src/app/+my-library/my-library.component.ts create mode 100644 client/src/app/+my-library/my-library.module.ts create mode 100644 client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html create mode 100644 client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss create mode 100644 client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts create mode 100644 client/src/app/+my-library/my-ownership/my-ownership.component.html create mode 100644 client/src/app/+my-library/my-ownership/my-ownership.component.scss create mode 100644 client/src/app/+my-library/my-ownership/my-ownership.component.ts create mode 100644 client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html create mode 100644 client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss create mode 100644 client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts create mode 100644 client/src/app/+my-library/my-video-imports/my-video-imports.component.html create mode 100644 client/src/app/+my-library/my-video-imports/my-video-imports.component.scss create mode 100644 client/src/app/+my-library/my-video-imports/my-video-imports.component.ts create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss create mode 100644 client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts create mode 100644 client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html create mode 100644 client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss create mode 100644 client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts create mode 100644 client/src/app/+my-library/my-videos/my-videos.component.html create mode 100644 client/src/app/+my-library/my-videos/my-videos.component.scss create mode 100644 client/src/app/+my-library/my-videos/my-videos.component.ts (limited to 'client/src') diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts deleted file mode 100644 index e2ea87fb8..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Component, OnInit } from '@angular/core' -import { Router } from '@angular/router' -import { AuthService, Notifier } from '@app/core' -import { - VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, - VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, - VIDEO_CHANNEL_NAME_VALIDATOR, - VIDEO_CHANNEL_SUPPORT_VALIDATOR -} from '@app/shared/form-validators/video-channel-validators' -import { FormValidatorService } from '@app/shared/shared-forms' -import { VideoChannelService } from '@app/shared/shared-main' -import { VideoChannelCreate } from '@shared/models' -import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' - -@Component({ - selector: 'my-account-video-channel-create', - templateUrl: './my-account-video-channel-edit.component.html', - styleUrls: [ './my-account-video-channel-edit.component.scss' ] -}) -export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelEdit implements OnInit { - error: string - - constructor ( - protected formValidatorService: FormValidatorService, - private authService: AuthService, - private notifier: Notifier, - private router: Router, - private videoChannelService: VideoChannelService - ) { - super() - } - - get instanceHost () { - return window.location.host - } - - ngOnInit () { - this.buildForm({ - name: VIDEO_CHANNEL_NAME_VALIDATOR, - 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, - description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, - support: VIDEO_CHANNEL_SUPPORT_VALIDATOR - }) - } - - formValidated () { - this.error = undefined - - const body = this.form.value - const videoChannelCreate: VideoChannelCreate = { - name: body.name, - displayName: body['display-name'], - description: body.description || null, - support: body.support || null - } - - this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( - () => { - this.authService.refreshUserInformation() - - this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) - this.router.navigate([ '/my-account', 'video-channels' ]) - }, - - err => { - if (err.status === 409) { - this.error = $localize`This name already exists on this instance.` - return - } - - this.error = err.message - } - ) - } - - isCreation () { - return true - } - - getFormButtonTitle () { - return $localize`Create` - } -} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html deleted file mode 100644 index 048d143cd..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html +++ /dev/null @@ -1,105 +0,0 @@ - - -
{{ error }}
- -
- -
-
-
NEW CHANNEL
-
CHANNEL
-
- -
- -
- -
- -
- @{{ instanceHost }} -
-
-
- {{ formErrors['name'] }} -
-
- - - -
- - -
- {{ formErrors['display-name'] }} -
-
- -
- - -
- {{ formErrors.description }} -
-
- -
- - - -
- {{ formErrors.support }} -
-
- -
- -
- -
-
- -
-
-
- -
-
-
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss deleted file mode 100644 index 8f8af655c..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss +++ /dev/null @@ -1,67 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -label { - font-weight: $font-regular; - font-size: 100%; -} - -.video-channel-title { - @include settings-big-title; -} - -my-actor-avatar-info { - display: block; - margin-bottom: 20px; -} - -.input-group { - @include peertube-input-group(fit-content); -} - -.input-group-append { - height: 30px; -} - -input { - &[type=text] { - @include peertube-input-text(340px); - - display: block; - - &#name { - width: auto; - flex-grow: 1; - } - } - - &[type=submit] { - @include peertube-button; - @include orange-button; - margin-left: auto; - } -} - -textarea { - @include peertube-textarea(500px, 150px); - - display: block; -} - -.peertube-select-container { - @include peertube-select-container(340px); -} - -.breadcrumb { - @include breadcrumb; -} - -@media screen and (max-width: $small-view) { - input[type=text]#name { - width: auto !important; - } - - label[for=name] + div, textarea { - width: 100%; - } -} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts deleted file mode 100644 index 710c51d8e..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FormReactive } from '@app/shared/shared-forms' -import { VideoChannel } from '@app/shared/shared-main' - -export abstract class MyAccountVideoChannelEdit extends FormReactive { - // We need it even in the create component because it's used in the edit template - videoChannelToUpdate: VideoChannel - instanceHost: string - - abstract isCreation (): boolean - abstract getFormButtonTitle (): string - - // We need this method so angular does not complain in child template that doesn't need this - onAvatarChange (formData: FormData) { /* empty */ } - - // Should be implemented by the child - isBulkUpdateVideosDisplayed () { - return false - } -} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts deleted file mode 100644 index 01659b8da..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Subscription } from 'rxjs' -import { Component, OnDestroy, OnInit } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, Notifier, ServerService } from '@app/core' -import { - VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, - VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, - VIDEO_CHANNEL_SUPPORT_VALIDATOR -} from '@app/shared/form-validators/video-channel-validators' -import { FormValidatorService } from '@app/shared/shared-forms' -import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' -import { ServerConfig, VideoChannelUpdate } from '@shared/models' -import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' - -@Component({ - selector: 'my-account-video-channel-update', - templateUrl: './my-account-video-channel-edit.component.html', - styleUrls: [ './my-account-video-channel-edit.component.scss' ] -}) -export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { - error: string - videoChannelToUpdate: VideoChannel - - private paramsSub: Subscription - private oldSupportField: string - private serverConfig: ServerConfig - - constructor ( - protected formValidatorService: FormValidatorService, - private authService: AuthService, - private notifier: Notifier, - private router: Router, - private route: ActivatedRoute, - private videoChannelService: VideoChannelService, - private serverService: ServerService - ) { - super() - } - - ngOnInit () { - this.serverConfig = this.serverService.getTmpConfig() - this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) - - this.buildForm({ - 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, - description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, - support: VIDEO_CHANNEL_SUPPORT_VALIDATOR, - bulkVideosSupportUpdate: null - }) - - this.paramsSub = this.route.params.subscribe(routeParams => { - const videoChannelId = routeParams['videoChannelId'] - - this.videoChannelService.getVideoChannel(videoChannelId).subscribe( - videoChannelToUpdate => { - this.videoChannelToUpdate = videoChannelToUpdate - - this.oldSupportField = videoChannelToUpdate.support - - this.form.patchValue({ - 'display-name': videoChannelToUpdate.displayName, - description: videoChannelToUpdate.description, - support: videoChannelToUpdate.support - }) - }, - - err => this.error = err.message - ) - }) - } - - ngOnDestroy () { - if (this.paramsSub) this.paramsSub.unsubscribe() - } - - formValidated () { - this.error = undefined - - const body = this.form.value - const videoChannelUpdate: VideoChannelUpdate = { - displayName: body['display-name'], - description: body.description || null, - support: body.support || null, - bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false - } - - this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( - () => { - this.authService.refreshUserInformation() - - this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`) - - this.router.navigate([ '/my-account', 'video-channels' ]) - }, - - err => this.error = err.message - ) - } - - onAvatarChange (formData: FormData) { - this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) - .subscribe( - data => { - this.notifier.success($localize`Avatar changed.`) - - this.videoChannelToUpdate.updateAvatar(data.avatar) - }, - - err => this.notifier.error(err.message) - ) - } - - get maxAvatarSize () { - return this.serverConfig.avatar.file.size.max - } - - get avatarExtensions () { - return this.serverConfig.avatar.file.extensions.join(',') - } - - isCreation () { - return false - } - - getFormButtonTitle () { - return $localize`Update` - } - - isBulkUpdateVideosDisplayed () { - if (this.oldSupportField === undefined) return false - - return this.oldSupportField !== this.form.value['support'] - } -} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts deleted file mode 100644 index 3aa3e360f..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { MyAccountVideoChannelUpdateComponent } from './my-account-video-channel-update.component' -import { MyAccountVideoChannelCreateComponent } from './my-account-video-channel-create.component' -import { MyAccountVideoChannelsComponent } from './my-account-video-channels.component' - -const myAccountVideoChannelsRoutes: Routes = [ - { - path: '', - component: MyAccountVideoChannelsComponent, - data: { - meta: { - title: $localize`Account video channels` - } - } - }, - { - path: 'create', - component: MyAccountVideoChannelCreateComponent, - data: { - meta: { - title: $localize`Create new video channel` - } - } - }, - { - path: 'update/:videoChannelId', - component: MyAccountVideoChannelUpdateComponent, - data: { - meta: { - title: $localize`Update video channel` - } - } - } -] - -@NgModule({ - imports: [ RouterModule.forChild(myAccountVideoChannelsRoutes) ], - exports: [ RouterModule ] -}) -export class MyAccountVideoChannelsRoutingModule {} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html deleted file mode 100644 index 205d23cd5..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html +++ /dev/null @@ -1,49 +0,0 @@ -

- - - My channels - {{ totalItems }} - -

- -
-
- - - Clear filters -
- - - - Create video channel - -
- -
-
- - Avatar - - -
- -
{{ videoChannel.displayName }}
-
{{ videoChannel.nameWithHost }}
-
- -
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
- -
{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}
- -
- - -
- -
- -
-
-
-
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss deleted file mode 100644 index f2f42459f..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss +++ /dev/null @@ -1,125 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.create-button { - @include create-button; -} - -input[type=text] { - @include peertube-input-text(300px); -} - -::ng-deep .action-button { - &.action-button-edit { - margin-right: 10px; - } -} - -.video-channel { - @include row-blocks; - padding-bottom: 0; - - img { - @include avatar(80px); - - margin-right: 10px; - } - - .video-channel-info { - flex-grow: 1; - - a.video-channel-names { - @include disable-default-a-behaviour; - - width: fit-content; - display: flex; - align-items: baseline; - color: pvar(--mainForegroundColor); - - .video-channel-display-name { - font-weight: $font-semibold; - font-size: 18px; - } - - .video-channel-name { - font-size: 14px; - color: $grey-actor-name; - margin-left: 5px; - } - } - } - - .video-channel-buttons { - margin-top: 10px; - min-width: 190px; - } -} - -::ng-deep .chartjs-render-monitor { - position: relative; - top: 1px; -} - -.video-channels-header { - margin-bottom: 30px; -} - -@media screen and (max-width: $small-view) { - .video-channels-header { - text-align: center; - } - - .video-channel { - padding-bottom: 10px; - - .video-channel-info { - padding-bottom: 10px; - text-align: center; - - .video-channel-names { - flex-direction: column; - align-items: center !important; - margin: auto; - - .video-channel-name { - margin-left: 0px !important; - } - } - } - - img { - margin-right: 0; - } - - .video-channel-buttons { - align-self: center; - } - } -} - -@media screen and (max-width: $mobile-view) { - .video-channels-header { - flex-direction: column; - - input[type=text] { - width: 100% !important; - margin-bottom: 12px; - } - } -} - -@media screen and (min-width: breakpoint(lg)) { - :host-context(.main-col:not(.expanded)) { - .video-channel-buttons { - float: right; - } - } -} - -@media screen and (min-width: $small-view) { - :host-context(.expanded) { - .video-channel-buttons { - float: right; - } - } -} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts deleted file mode 100644 index 281801ff6..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { ChartData } from 'chart.js' -import { max, maxBy, min, minBy } from 'lodash-es' -import { Subject } from 'rxjs' -import { debounceTime, mergeMap } from 'rxjs/operators' -import { Component, OnInit } from '@angular/core' -import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' -import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' - -@Component({ - selector: 'my-account-video-channels', - templateUrl: './my-account-video-channels.component.html', - styleUrls: [ './my-account-video-channels.component.scss' ] -}) -export class MyAccountVideoChannelsComponent implements OnInit { - totalItems: number - - videoChannels: VideoChannel[] = [] - videoChannelsChartData: ChartData[] - videoChannelsMinimumDailyViews = 0 - videoChannelsMaximumDailyViews: number - - channelsSearch: string - channelsSearchChanged = new Subject() - - private user: User - - constructor ( - private authService: AuthService, - private notifier: Notifier, - private confirmService: ConfirmService, - private videoChannelService: VideoChannelService, - private screenService: ScreenService - ) {} - - ngOnInit () { - this.user = this.authService.getUser() - - this.loadVideoChannels() - - this.channelsSearchChanged - .pipe(debounceTime(500)) - .subscribe(() => { - this.loadVideoChannels() - }) - } - - get isInSmallView () { - return this.screenService.isInSmallView() - } - - get chartOptions () { - return { - legend: { - display: false - }, - scales: { - xAxes: [{ - display: false - }], - yAxes: [{ - display: false, - ticks: { - min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)), - max: Math.max(1, this.videoChannelsMaximumDailyViews) - } - }] - }, - layout: { - padding: { - left: 15, - right: 15, - top: 10, - bottom: 0 - } - }, - elements: { - point: { - radius: 0 - } - }, - tooltips: { - mode: 'index', - intersect: false, - custom: function (tooltip: any) { - if (!tooltip) return - // disable displaying the color box - tooltip.displayColors = false - }, - callbacks: { - label: (tooltip: any, data: any) => `${tooltip.value} views` - } - }, - hover: { - mode: 'index', - intersect: false - } - } - } - - resetSearch () { - this.channelsSearch = '' - this.onChannelsSearchChanged() - } - - onChannelsSearchChanged () { - this.channelsSearchChanged.next() - } - - async deleteVideoChannel (videoChannel: VideoChannel) { - const res = await this.confirmService.confirmWithInput( - $localize`Do you really want to delete ${videoChannel.displayName}? -It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another -channel with the same name (${videoChannel.name})!`, - - $localize`Please type the display name of the video channel (${videoChannel.displayName}) to confirm`, - - videoChannel.displayName, - - $localize`Delete` - ) - if (res === false) return - - this.videoChannelService.removeVideoChannel(videoChannel) - .subscribe( - () => { - this.loadVideoChannels() - this.notifier.success($localize`Video channel ${videoChannel.displayName} deleted.`) - }, - - error => this.notifier.error(error.message) - ) - } - - private loadVideoChannels () { - this.authService.userInformationLoaded - .pipe(mergeMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch))) - .subscribe(res => { - this.videoChannels = res.data - this.totalItems = res.total - - // chart data - this.videoChannelsChartData = this.videoChannels.map(v => ({ - labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), - datasets: [ - { - label: $localize`Views for the day`, - data: v.viewsPerDay.map(day => day.views), - fill: false, - borderColor: '#c6c6c6' - } - ] - } as ChartData)) - - // chart options that depend on chart data: - // we don't want to skew values and have min at 0, so we define what the floor/ceiling is here - this.videoChannelsMinimumDailyViews = min( - // compute local minimum daily views for each channel, by their "views" attribute - this.videoChannels.map(v => minBy( - v.viewsPerDay, - day => day.views - ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute - ) - this.videoChannelsMaximumDailyViews = max( - // compute local maximum daily views for each channel, by their "views" attribute - this.videoChannels.map(v => maxBy( - v.viewsPerDay, - day => day.views - ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute - ) - }) - } -} diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts deleted file mode 100644 index f8c6ad56b..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ChartModule } from 'primeng/chart' -import { NgModule } from '@angular/core' -import { SharedFormModule } from '@app/shared/shared-forms' -import { SharedGlobalIconModule } from '@app/shared/shared-icons' -import { SharedMainModule } from '@app/shared/shared-main' -import { MyAccountVideoChannelCreateComponent } from './my-account-video-channel-create.component' -import { MyAccountVideoChannelUpdateComponent } from './my-account-video-channel-update.component' -import { MyAccountVideoChannelsRoutingModule } from './my-account-video-channels-routing.module' -import { MyAccountVideoChannelsComponent } from './my-account-video-channels.component' - -@NgModule({ - imports: [ - MyAccountVideoChannelsRoutingModule, - - ChartModule, - - SharedMainModule, - SharedFormModule, - SharedGlobalIconModule - ], - - declarations: [ - MyAccountVideoChannelsComponent, - MyAccountVideoChannelCreateComponent, - MyAccountVideoChannelUpdateComponent - ], - - exports: [], - providers: [] -}) -export class MyAccountVideoChannelsModule { } diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html deleted file mode 100644 index cff46a41d..000000000 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.html +++ /dev/null @@ -1,28 +0,0 @@ -

- - My history -

- -
-
- - -
- - -
- - -
You don't have any video history yet.
- -
-
- -
-
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss deleted file mode 100644 index 9eeeaf310..000000000 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.scss +++ /dev/null @@ -1,59 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.no-history { - display: flex; - justify-content: center; - margin-top: 50px; - font-weight: $font-semibold; - font-size: 16px; -} - -.top-buttons { - margin-bottom: 20px; - display: flex; - align-items: center; - flex-wrap: wrap; - - .history-switch { - display: flex; - flex-grow: 1; - - label { - margin: 0 0 0 5px; - } - } - - .delete-history { - @include peertube-button; - @include grey-button; - @include button-with-icon; - - font-size: 15px; - } -} - -.video { - @include row-blocks; - - .my-video-miniature { - flex-grow: 1; - } -} - -@media screen and (max-width: $mobile-view) { - .top-buttons { - .history-switch label, .delete-history { - @include ellipsis; - } - - .history-switch label { - width: 60%; - } - - .delete-history { - margin-left: auto; - max-width: 32%; - } - } -} diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts deleted file mode 100644 index 3298c56c7..000000000 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { - AuthService, - ComponentPagination, - ConfirmService, - LocalStorageService, - Notifier, - ScreenService, - ServerService, - UserService -} from '@app/core' -import { immutableAssign } from '@app/helpers' -import { UserHistoryService } from '@app/shared/shared-main' -import { AbstractVideoList } from '@app/shared/shared-video-miniature' - -@Component({ - selector: 'my-account-history', - templateUrl: './my-account-history.component.html', - styleUrls: [ './my-account-history.component.scss' ] -}) -export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { - titlePage: string - pagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 5, - totalItems: null - } - videosHistoryEnabled: boolean - - constructor ( - protected router: Router, - protected serverService: ServerService, - protected route: ActivatedRoute, - protected authService: AuthService, - protected userService: UserService, - protected notifier: Notifier, - protected screenService: ScreenService, - protected storageService: LocalStorageService, - private confirmService: ConfirmService, - private userHistoryService: UserHistoryService - ) { - super() - - this.titlePage = $localize`My videos history` - } - - ngOnInit () { - super.ngOnInit() - - this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled - } - - ngOnDestroy () { - super.ngOnDestroy() - } - - getVideosObservable (page: number) { - const newPagination = immutableAssign(this.pagination, { currentPage: page }) - - return this.userHistoryService.getUserVideosHistory(newPagination) - } - - generateSyndicationList () { - throw new Error('Method not implemented.') - } - - onVideosHistoryChange () { - this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled }) - .subscribe( - () => { - const message = this.videosHistoryEnabled === true ? - $localize`Videos history is enabled` : - $localize`Videos history is disabled` - - this.notifier.success(message) - - this.authService.refreshUserInformation() - }, - - err => this.notifier.error(err.message) - ) - } - - async deleteHistory () { - const title = $localize`Delete videos history` - const message = $localize`Are you sure you want to delete all your videos history?` - - const res = await this.confirmService.confirm(message, title) - if (res !== true) return - - this.userHistoryService.deleteUserVideosHistory() - .subscribe( - () => { - this.notifier.success($localize`Videos history deleted`) - - this.reloadVideos() - }, - - err => this.notifier.error(err.message) - ) - } -} diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html deleted file mode 100644 index def1cbab6..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss deleted file mode 100644 index c7357f62d..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -select { - display: block; -} - -.peertube-select-container { - @include peertube-select-container(350px); -} - -.form-group { - margin: 20px 0; -} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts deleted file mode 100644 index 4c4436755..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' -import { AuthService, Notifier } from '@app/core' -import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' -import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { VideoChangeOwnership, VideoChannel } from '@shared/models' - -@Component({ - selector: 'my-account-accept-ownership', - templateUrl: './my-account-accept-ownership.component.html', - styleUrls: [ './my-account-accept-ownership.component.scss' ] -}) -export class MyAccountAcceptOwnershipComponent extends FormReactive implements OnInit { - @Output() accepted = new EventEmitter() - - @ViewChild('modal', { static: true }) modal: ElementRef - - videoChangeOwnership: VideoChangeOwnership | undefined = undefined - - videoChannels: VideoChannel[] - - error: string = null - - constructor ( - protected formValidatorService: FormValidatorService, - private videoOwnershipService: VideoOwnershipService, - private notifier: Notifier, - private authService: AuthService, - private videoChannelService: VideoChannelService, - private modalService: NgbModal - ) { - super() - } - - ngOnInit () { - this.videoChannels = [] - - this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account) - .subscribe(videoChannels => this.videoChannels = videoChannels.data) - - this.buildForm({ - channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR - }) - } - - show (videoChangeOwnership: VideoChangeOwnership) { - this.videoChangeOwnership = videoChangeOwnership - this.modalService - .open(this.modal, { centered: true }) - .result - .then(() => this.acceptOwnership()) - .catch(() => this.videoChangeOwnership = undefined) - } - - acceptOwnership () { - const channel = this.form.value['channel'] - - const videoChangeOwnership = this.videoChangeOwnership - this.videoOwnershipService - .acceptOwnership(videoChangeOwnership.id, { channelId: channel }) - .subscribe( - () => { - this.notifier.success($localize`Ownership accepted`) - if (this.accepted) this.accepted.emit() - this.videoChangeOwnership = undefined - }, - - err => this.notifier.error(err.message) - ) - } -} diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html deleted file mode 100644 index fd2163fb4..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ /dev/null @@ -1,90 +0,0 @@ -

- - My ownership changes -

- - - - - Actions - Initiator - Video - - Created - - - Status - - - - - - - - - - - - - -
- Avatar -
- {{ videoChangeOwnership.initiatorAccount.displayName }} - {{ videoChangeOwnership.initiatorAccount.nameWithHost }} -
-
-
- - - - -
-
- -
-
-
- {{ videoChangeOwnership.video.name }} -
-
by {{ videoChangeOwnership.video.channel?.displayName }}
-
-
-
- - - {{ videoChangeOwnership.createdAt | date: 'short' }} - - - {{ videoChangeOwnership.status }} - - -
- - - - -
- No ownership change request found. -
- - -
-
- - diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss deleted file mode 100644 index 7cac9c9f3..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss +++ /dev/null @@ -1,71 +0,0 @@ -@import 'miniature'; -@import 'mixins'; - -.chip { - @include chip; -} - -.badge { - @include table-badge; -} - -.video-table-video { - display: inline-flex; - - .video-table-video-image { - @include miniature-thumbnail; - - $image-height: 45px; - - height: $image-height; - width: #{(16/9) * $image-height}; - margin-right: 0.5rem; - border-radius: 2px; - border: none; - background: transparent; - display: inline-flex; - justify-content: center; - align-items: center; - position: relative; - - img { - height: 100%; - width: 100%; - border-radius: 2px; - } - - span { - color: pvar(--inputPlaceholderColor); - } - - .video-table-video-image-label { - @include static-thumbnail-overlay; - position: absolute; - border-radius: 3px; - font-size: 10px; - padding: 0 3px; - line-height: 1.3; - bottom: 2px; - right: 2px; - } - } - - .video-table-video-text { - display: inline-flex; - flex-direction: column; - justify-content: center; - font-size: 90%; - color: pvar(--mainForegroundColor); - line-height: 1rem; - - div .glyphicon { - font-size: 80%; - color: gray; - margin-left: 0.1rem; - } - - div + div { - font-size: 80%; - } - } -} diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts deleted file mode 100644 index 7473470aa..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { SortMeta } from 'primeng/api' -import { Component, OnInit, ViewChild } from '@angular/core' -import { Notifier, RestPagination, RestTable } from '@app/core' -import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' -import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models' -import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' -import { getAbsoluteAPIUrl } from '@app/helpers' - -@Component({ - selector: 'my-account-ownership', - templateUrl: './my-account-ownership.component.html', - styleUrls: [ './my-account-ownership.component.scss' ] -}) -export class MyAccountOwnershipComponent extends RestTable implements OnInit { - videoChangeOwnerships: VideoChangeOwnership[] = [] - totalRecords = 0 - sort: SortMeta = { field: 'createdAt', order: -1 } - pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - - @ViewChild('myAccountAcceptOwnershipComponent', { static: true }) myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent - - constructor ( - private notifier: Notifier, - private videoOwnershipService: VideoOwnershipService - ) { - super() - } - - ngOnInit () { - this.initialize() - } - - getIdentifier () { - return 'MyAccountOwnershipComponent' - } - - getStatusClass (status: VideoChangeOwnershipStatus) { - switch (status) { - case VideoChangeOwnershipStatus.ACCEPTED: - return 'badge-green' - case VideoChangeOwnershipStatus.REFUSED: - return 'badge-red' - default: - return 'badge-yellow' - } - } - - switchToDefaultAvatar ($event: Event) { - ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() - } - - openAcceptModal (videoChangeOwnership: VideoChangeOwnership) { - this.myAccountAcceptOwnershipComponent.show(videoChangeOwnership) - } - - accepted () { - this.loadData() - } - - refuse (videoChangeOwnership: VideoChangeOwnership) { - this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) - .subscribe( - () => this.loadData(), - err => this.notifier.error(err.message) - ) - } - - protected loadData () { - return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) - .subscribe( - resultList => { - this.videoChangeOwnerships = resultList.data.map(change => ({ - ...change, - initiatorAccount: new Account(change.initiatorAccount), - nextOwnerAccount: new Account(change.nextOwnerAccount) - })) - this.totalRecords = resultList.total - }, - - err => this.notifier.error(err.message) - ) - } -} diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 0bcb38ef5..81380ec6e 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -2,21 +2,12 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' import { MetaGuard } from '@ngx-meta/core' import { LoginGuard } from '../core' +import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' -import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component' import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' -import { MyAccountOwnershipComponent } from './my-account-ownership/my-account-ownership.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' -import { MyAccountSubscriptionsComponent } from './my-account-subscriptions/my-account-subscriptions.component' -import { MyAccountVideoImportsComponent } from './my-account-video-imports/my-account-video-imports.component' -import { MyAccountVideoPlaylistCreateComponent } from './my-account-video-playlists/my-account-video-playlist-create.component' -import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-playlists/my-account-video-playlist-elements.component' -import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' -import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' -import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' import { MyAccountComponent } from './my-account.component' -import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' const myAccountRoutes: Routes = [ { @@ -41,88 +32,50 @@ const myAccountRoutes: Routes = [ { path: 'video-channels', - loadChildren: () => { - return import('./+my-account-video-channels/my-account-video-channels.module') - .then(m => m.MyAccountVideoChannelsModule) - } + redirectTo: '/my-library/video-channels', + pathMatch: 'full' }, { path: 'video-playlists', - component: MyAccountVideoPlaylistsComponent, - data: { - meta: { - title: $localize`Account playlists` - } - } + redirectTo: '/my-library/video-playlists', + pathMatch: 'full' }, { path: 'video-playlists/create', - component: MyAccountVideoPlaylistCreateComponent, - data: { - meta: { - title: $localize`Create new playlist` - } - } + redirectTo: '/my-library/video-playlists/create', + pathMatch: 'full' }, { path: 'video-playlists/:videoPlaylistId', - component: MyAccountVideoPlaylistElementsComponent, - data: { - meta: { - title: $localize`Playlist elements` - } - } + redirectTo: '/my-library/video-playlists/:videoPlaylistId', + pathMatch: 'full' }, { path: 'video-playlists/update/:videoPlaylistId', - component: MyAccountVideoPlaylistUpdateComponent, - data: { - meta: { - title: $localize`Update playlist` - } - } + redirectTo: '/my-library/video-playlists/update/:videoPlaylistId', + pathMatch: 'full' }, { path: 'videos', - component: MyAccountVideosComponent, - data: { - meta: { - title: $localize`Account videos` - }, - reuse: { - enabled: true, - key: 'my-account-videos-list' - } - } + redirectTo: '/my-library/videos', + pathMatch: 'full' }, { path: 'video-imports', - component: MyAccountVideoImportsComponent, - data: { - meta: { - title: $localize`Account video imports` - } - } + redirectTo: '/my-library/video-imports', + pathMatch: 'full' }, { path: 'subscriptions', - component: MyAccountSubscriptionsComponent, - data: { - meta: { - title: $localize`Account subscriptions` - } - } + redirectTo: '/my-library/subscriptions', + pathMatch: 'full' }, { path: 'ownership', - component: MyAccountOwnershipComponent, - data: { - meta: { - title: $localize`Ownership changes` - } - } + redirectTo: '/my-library/ownership', + pathMatch: 'full' }, { path: 'blocklist/accounts', @@ -144,16 +97,8 @@ const myAccountRoutes: Routes = [ }, { path: 'history/videos', - component: MyAccountHistoryComponent, - data: { - meta: { - title: $localize`Videos history` - }, - reuse: { - enabled: true, - key: 'my-videos-history-list' - } - } + redirectTo: '/my-library/history/videos', + pathMatch: 'full' }, { path: 'notifications', diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html deleted file mode 100644 index 6ab3826ba..000000000 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html +++ /dev/null @@ -1,42 +0,0 @@ -

- - - My subscriptions - {{ pagination.totalItems }} - -

- -
-
- - - Clear filters -
-
- -
You don't have any subscriptions yet.
- - diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss deleted file mode 100644 index 5ead45dd8..000000000 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss +++ /dev/null @@ -1,81 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -input[type=text] { - @include peertube-input-text(300px); -} - -.video-channel { - @include row-blocks; - - img { - @include avatar(80px); - - margin-right: 10px; - } - - .video-channel-info { - flex-grow: 1; - - a.video-channel-names { - @include disable-default-a-behaviour; - - width: fit-content; - display: flex; - align-items: baseline; - color: pvar(--mainForegroundColor); - - .video-channel-display-name { - font-weight: $font-semibold; - font-size: 18px; - } - - .video-channel-name { - font-size: 14px; - color: $grey-actor-name; - margin-left: 5px; - } - } - } - - .actor-owner { - @include actor-owner; - - margin-top: 0; - } -} - -.video-subscriptions-header { - margin-bottom: 30px; -} - -@media screen and (max-width: $small-view) { - .video-channel { - .video-channel-info { - padding-bottom: 10px; - text-align: center; - - .video-channel-names { - flex-direction: column; - align-items: center !important; - margin: auto; - } - } - - img { - margin-right: 0; - } - } -} - -@media screen and (max-width: $mobile-view) { - .video-subscriptions-header { - flex-direction: column; - - input[type=text] { - width: 100% !important; - } - } -} - - diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts deleted file mode 100644 index 994fe5142..000000000 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Subject } from 'rxjs' -import { Component, OnInit } from '@angular/core' -import { ComponentPagination, Notifier } from '@app/core' -import { VideoChannel } from '@app/shared/shared-main' -import { UserSubscriptionService } from '@app/shared/shared-user-subscription' -import { debounceTime } from 'rxjs/operators' - -@Component({ - selector: 'my-account-subscriptions', - templateUrl: './my-account-subscriptions.component.html', - styleUrls: [ './my-account-subscriptions.component.scss' ] -}) -export class MyAccountSubscriptionsComponent implements OnInit { - videoChannels: VideoChannel[] = [] - - pagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 10, - totalItems: null - } - - onDataSubject = new Subject() - - subscriptionsSearch: string - subscriptionsSearchChanged = new Subject() - - constructor ( - private userSubscriptionService: UserSubscriptionService, - private notifier: Notifier - ) {} - - ngOnInit () { - this.loadSubscriptions() - - this.subscriptionsSearchChanged - .pipe(debounceTime(500)) - .subscribe(() => { - this.pagination.currentPage = 1 - this.loadSubscriptions(false) - }) - } - - resetSearch () { - this.subscriptionsSearch = '' - this.onSubscriptionsSearchChanged() - } - - onSubscriptionsSearchChanged () { - this.subscriptionsSearchChanged.next() - } - - onNearOfBottom () { - // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return - - this.pagination.currentPage += 1 - this.loadSubscriptions() - } - - private loadSubscriptions (more = true) { - this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch }) - .subscribe( - res => { - this.videoChannels = more - ? this.videoChannels.concat(res.data) - : res.data - this.pagination.totalItems = res.total - - this.onDataSubject.next(res.data) - }, - - error => this.notifier.error(error.message) - ) - } -} diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html deleted file mode 100644 index 1d3a45f76..000000000 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html +++ /dev/null @@ -1,70 +0,0 @@ -

- - My imports -

- - - - - - Action - Target - Video - State - Created - - - - - - - - - - - - - - - - - {{ videoImport.targetUrl }} - - {{ videoImport.torrentName || videoImport.magnetUri }} - - - - - {{ videoImport.video?.name }} - - {{ videoImport.video?.name }} - - This video was deleted - - - - - - {{ videoImport.state.label }} - - - - {{ videoImport.createdAt | date: 'short' }} - - - - - - -
{{ videoImport.error }}
- - -
-
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss deleted file mode 100644 index a93c28028..000000000 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -pre { - font-size: 11px; -} - -.video-import-error { - color: red; -} - -.badge { - @include table-badge; -} diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts deleted file mode 100644 index 9dd5ef142..000000000 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { SortMeta } from 'primeng/api' -import { Component, OnInit } from '@angular/core' -import { Notifier, RestPagination, RestTable } from '@app/core' -import { VideoImportService } from '@app/shared/shared-main' -import { VideoImport, VideoImportState } from '@shared/models' - -@Component({ - selector: 'my-account-video-imports', - templateUrl: './my-account-video-imports.component.html', - styleUrls: [ './my-account-video-imports.component.scss' ] -}) -export class MyAccountVideoImportsComponent extends RestTable implements OnInit { - videoImports: VideoImport[] = [] - totalRecords = 0 - sort: SortMeta = { field: 'createdAt', order: 1 } - pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - - constructor ( - private notifier: Notifier, - private videoImportService: VideoImportService - ) { - super() - } - - ngOnInit () { - this.initialize() - } - - getIdentifier () { - return 'MyAccountVideoImportsComponent' - } - - getVideoImportStateClass (state: VideoImportState) { - switch (state) { - case VideoImportState.FAILED: - return 'badge-red' - case VideoImportState.REJECTED: - return 'badge-banned' - case VideoImportState.PENDING: - return 'badge-yellow' - default: - return 'badge-green' - } - } - - isVideoImportSuccess (videoImport: VideoImport) { - return videoImport.state.id === VideoImportState.SUCCESS - } - - isVideoImportPending (videoImport: VideoImport) { - return videoImport.state.id === VideoImportState.PENDING - } - - isVideoImportFailed (videoImport: VideoImport) { - return videoImport.state.id === VideoImportState.FAILED - } - - getVideoUrl (video: { uuid: string }) { - return '/videos/watch/' + video.uuid - } - - getEditVideoUrl (video: { uuid: string }) { - return '/videos/update/' + video.uuid - } - - protected loadData () { - this.videoImportService.getMyVideoImports(this.pagination, this.sort) - .subscribe( - resultList => { - this.videoImports = resultList.data - this.totalRecords = resultList.total - }, - - err => this.notifier.error(err.message) - ) - } -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts deleted file mode 100644 index 7a80aaa92..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Component, OnInit } from '@angular/core' -import { Router } from '@angular/router' -import { AuthService, Notifier, ServerService } from '@app/core' -import { populateAsyncUserVideoChannels } from '@app/helpers' -import { - setPlaylistChannelValidator, - VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, - VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, - VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, - VIDEO_PLAYLIST_PRIVACY_VALIDATOR -} from '@app/shared/form-validators/video-playlist-validators' -import { FormValidatorService } from '@app/shared/shared-forms' -import { VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' -import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' -import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit' - -@Component({ - selector: 'my-account-video-playlist-create', - templateUrl: './my-account-video-playlist-edit.component.html', - styleUrls: [ './my-account-video-playlist-edit.component.scss' ] -}) -export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit { - error: string - - constructor ( - protected formValidatorService: FormValidatorService, - private authService: AuthService, - private notifier: Notifier, - private router: Router, - private videoPlaylistService: VideoPlaylistService, - private serverService: ServerService - ) { - super() - } - - ngOnInit () { - this.buildForm({ - displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, - privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR, - description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, - videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, - thumbnailfile: null - }) - - this.form.get('privacy').valueChanges.subscribe(privacy => { - setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) - }) - - populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) - .catch(err => console.error('Cannot populate user video channels.', err)) - - this.serverService.getVideoPlaylistPrivacies() - .subscribe(videoPlaylistPrivacies => { - this.videoPlaylistPrivacies = videoPlaylistPrivacies - - this.form.patchValue({ - privacy: VideoPlaylistPrivacy.PRIVATE - }) - }) - } - - formValidated () { - this.error = undefined - - const body = this.form.value - const videoPlaylistCreate: VideoPlaylistCreate = { - displayName: body.displayName, - privacy: body.privacy, - description: body.description || null, - videoChannelId: body.videoChannelId || null, - thumbnailfile: body.thumbnailfile || null - } - - this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe( - () => { - this.notifier.success($localize`Playlist ${videoPlaylistCreate.displayName} created.`) - this.router.navigate([ '/my-account', 'video-playlists' ]) - }, - - err => this.error = err.message - ) - } - - isCreation () { - return true - } - - getFormButtonTitle () { - return $localize`Create` - } -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html deleted file mode 100644 index 56060359a..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html +++ /dev/null @@ -1,100 +0,0 @@ - - -
{{ error }}
- -
- -
-
-
NEW PLAYLIST
-
PLAYLIST
-
- -
- -
-
- - -
- {{ formErrors['displayName'] }} -
-
- -
- - -
- {{ formErrors.description }} -
-
-
- -
-
- -
- -
- -
- {{ formErrors.privacy }} -
-
- -
- - - - -
- {{ formErrors['videoChannelId'] }} -
-
- -
- - - -
-
- -
-
-
- -
-
-
-
- -
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss deleted file mode 100644 index 08fab1101..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss +++ /dev/null @@ -1,36 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -label { - font-weight: $font-regular; - font-size: 100%; -} - -.video-playlist-title { - @include settings-big-title; -} - -input[type=text] { - @include peertube-input-text(340px); - - display: block; -} - -textarea { - @include peertube-textarea(500px, 150px); - - display: block; -} - -.peertube-select-container { - @include peertube-select-container(340px); -} - -input[type=submit] { - @include peertube-button; - @include orange-button; -} - -.breadcrumb { - @include breadcrumb; -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts deleted file mode 100644 index 774d58c90..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms' -import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' -import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' - -export abstract class MyAccountVideoPlaylistEdit extends FormReactive { - // Declare it here to avoid errors in create template - videoPlaylistToUpdate: VideoPlaylist - userVideoChannels: SelectChannelItem[] = [] - videoPlaylistPrivacies: VideoConstant[] = [] - - abstract isCreation (): boolean - abstract getFormButtonTitle (): string -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html deleted file mode 100644 index 09b4c8a1b..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html +++ /dev/null @@ -1,51 +0,0 @@ -
- -
- - -
- - - -
- -
- -
-
-
No videos in this playlist.
- -
- Browse videos on PeerTube to add them in your playlist. -
- -
- See the documentation for more information. -
-
- -
-
- - -
-
-
-
- - diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss deleted file mode 100644 index de7e1993f..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss +++ /dev/null @@ -1,83 +0,0 @@ -@import '_variables'; -@import '_mixins'; -@import '_miniature'; - -.playlist-info { - background-color: pvar(--submenuColor); - margin-left: -$not-expanded-horizontal-margins; - margin-top: -$sub-menu-margin-bottom; - - padding: 10px; - - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - - /* fix ellipsis dots background color */ - ::ng-deep .miniature-name::after { - background-color: pvar(--submenuColor) !important; - } -} - -.playlist-buttons { - display:flex; - margin: 30px 0 10px 0; - - .share-button { - @include peertube-button; - @include button-with-icon(17px, 3px, -1px); - @include grey-button; - @include apply-svg-color(pvar(--actionButtonColor)); - - margin-right: 10px; - } -} - -// Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples -.cdk-drag-preview { - box-sizing: border-box; - border-radius: 4px; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), - 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12); -} - -.cdk-drag-placeholder { - opacity: 0; -} - -.cdk-drag-animating { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} - -.video:last-child { - border: none; -} - -.videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} - -@media screen and (max-width: $small-view) { - .playlist-info { - width: 100vw; - padding-top: 20px; - margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); - } - - .playlist-elements { - padding: 0 !important; - } - - ::ng-deep my-video-playlist-element-miniature { - - .video { - padding: 5px !important; - } - - .position { - margin-right: 5px !important; - } - } -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts deleted file mode 100644 index f6cdf1067..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { Subject, Subscription } from 'rxjs' -import { CdkDragDrop } from '@angular/cdk/drag-drop' -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { ComponentPagination, ConfirmService, Notifier, ScreenService } from '@app/core' -import { DropdownAction } from '@app/shared/shared-main' -import { VideoShareComponent } from '@app/shared/shared-share-modal' -import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { VideoPlaylistType } from '@shared/models' - -@Component({ - selector: 'my-account-video-playlist-elements', - templateUrl: './my-account-video-playlist-elements.component.html', - styleUrls: [ './my-account-video-playlist-elements.component.scss' ] -}) -export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { - @ViewChild('videoShareModal') videoShareModal: VideoShareComponent - - playlistElements: VideoPlaylistElement[] = [] - playlist: VideoPlaylist - - playlistActions: DropdownAction[][] = [] - - pagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 10, - totalItems: null - } - - onDataSubject = new Subject() - - private videoPlaylistId: string | number - private paramsSub: Subscription - - constructor ( - private notifier: Notifier, - private router: Router, - private confirmService: ConfirmService, - private route: ActivatedRoute, - private screenService: ScreenService, - private videoPlaylistService: VideoPlaylistService - ) {} - - ngOnInit () { - this.playlistActions = [ - [ - { - label: $localize`Update playlist`, - iconName: 'edit', - linkBuilder: playlist => [ '/my-account', 'video-playlists', 'update', playlist.uuid ] - }, - { - label: $localize`Delete playlist`, - iconName: 'delete', - handler: playlist => this.deleteVideoPlaylist(playlist) - } - ] - ] - - this.paramsSub = this.route.params.subscribe(routeParams => { - this.videoPlaylistId = routeParams[ 'videoPlaylistId' ] - this.loadElements() - - this.loadPlaylistInfo() - }) - } - - ngOnDestroy () { - if (this.paramsSub) this.paramsSub.unsubscribe() - } - - drop (event: CdkDragDrop) { - const previousIndex = event.previousIndex - const newIndex = event.currentIndex - - if (previousIndex === newIndex) return - - const oldPosition = this.playlistElements[previousIndex].position - let insertAfter = this.playlistElements[newIndex].position - - if (oldPosition > insertAfter) insertAfter-- - - const element = this.playlistElements[previousIndex] - - this.playlistElements.splice(previousIndex, 1) - this.playlistElements.splice(newIndex, 0, element) - - this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) - .subscribe( - () => { - this.reorderClientPositions() - }, - - err => this.notifier.error(err.message) - ) - } - - onElementRemoved (element: VideoPlaylistElement) { - const oldFirst = this.findFirst() - - this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) - this.reorderClientPositions(oldFirst) - } - - onNearOfBottom () { - // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return - - this.pagination.currentPage += 1 - this.loadElements() - } - - trackByFn (index: number, elem: VideoPlaylistElement) { - return elem.id - } - - isRegularPlaylist (playlist: VideoPlaylist) { - return playlist?.type.id === VideoPlaylistType.REGULAR - } - - showShareModal () { - this.videoShareModal.show() - } - - async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) { - const res = await this.confirmService.confirm( - $localize`Do you really want to delete ${videoPlaylist.displayName}?`, - $localize`Delete` - ) - if (res === false) return - - this.videoPlaylistService.removeVideoPlaylist(videoPlaylist) - .subscribe( - () => { - this.router.navigate([ '/my-account', 'video-playlists' ]) - this.notifier.success($localize`Playlist ${videoPlaylist.displayName} deleted.`) - }, - - error => this.notifier.error(error.message) - ) - } - - /** - * Returns null to not have drag and drop delay. - * In small views, where elements are about 100% wide, - * we add a delay to prevent unwanted drag&drop. - * - * @see {@link https://github.com/Chocobozzz/PeerTube/issues/2078} - * - * @returns {null|number} Null for no delay, or a number in milliseconds. - */ - getDragStartDelay (): null | number { - if (this.screenService.isInTouchScreen()) { - return 500 - } - - return null - } - - private loadElements () { - this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination) - .subscribe(({ total, data }) => { - this.playlistElements = this.playlistElements.concat(data) - this.pagination.totalItems = total - - this.onDataSubject.next(data) - }) - } - - private loadPlaylistInfo () { - this.videoPlaylistService.getVideoPlaylist(this.videoPlaylistId) - .subscribe(playlist => { - this.playlist = playlist - }) - } - - private reorderClientPositions (first?: VideoPlaylistElement) { - if (this.playlistElements.length === 0) return - - const oldFirst = first || this.findFirst() - let i = 1 - - for (const element of this.playlistElements) { - element.position = i - i++ - } - - // Reload playlist thumbnail if the first element changed - const newFirst = this.findFirst() - if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { - this.playlist.refreshThumbnail() - } - } - - private findFirst () { - return this.playlistElements.find(e => e.position === 1) - } -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts deleted file mode 100644 index fefc6d607..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { forkJoin, Subscription } from 'rxjs' -import { map, switchMap } from 'rxjs/operators' -import { Component, OnDestroy, OnInit } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, Notifier, ServerService } from '@app/core' -import { populateAsyncUserVideoChannels } from '@app/helpers' -import { - setPlaylistChannelValidator, - VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, - VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, - VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, - VIDEO_PLAYLIST_PRIVACY_VALIDATOR -} from '@app/shared/form-validators/video-playlist-validators' -import { FormValidatorService } from '@app/shared/shared-forms' -import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { VideoPlaylistUpdate } from '@shared/models' -import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit' - -@Component({ - selector: 'my-account-video-playlist-update', - templateUrl: './my-account-video-playlist-edit.component.html', - styleUrls: [ './my-account-video-playlist-edit.component.scss' ] -}) -export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy { - error: string - videoPlaylistToUpdate: VideoPlaylist - - private paramsSub: Subscription - - constructor ( - protected formValidatorService: FormValidatorService, - private authService: AuthService, - private notifier: Notifier, - private router: Router, - private route: ActivatedRoute, - private videoPlaylistService: VideoPlaylistService, - private serverService: ServerService - ) { - super() - } - - ngOnInit () { - this.buildForm({ - displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, - privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR, - description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, - videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, - thumbnailfile: null - }) - - this.form.get('privacy').valueChanges.subscribe(privacy => { - setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) - }) - - populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) - .catch(err => console.error('Cannot populate user video channels.', err)) - - this.paramsSub = this.route.params - .pipe( - map(routeParams => routeParams['videoPlaylistId']), - switchMap(videoPlaylistId => { - return forkJoin([ - this.videoPlaylistService.getVideoPlaylist(videoPlaylistId), - this.serverService.getVideoPlaylistPrivacies() - ]) - }) - ) - .subscribe( - ([ videoPlaylistToUpdate, videoPlaylistPrivacies]) => { - this.videoPlaylistToUpdate = videoPlaylistToUpdate - this.videoPlaylistPrivacies = videoPlaylistPrivacies - - this.hydrateFormFromPlaylist() - }, - - err => this.error = err.message - ) - } - - ngOnDestroy () { - if (this.paramsSub) this.paramsSub.unsubscribe() - } - - formValidated () { - this.error = undefined - - const body = this.form.value - const videoPlaylistUpdate: VideoPlaylistUpdate = { - displayName: body.displayName, - privacy: body.privacy, - description: body.description || null, - videoChannelId: body.videoChannelId || null, - thumbnailfile: body.thumbnailfile || undefined - } - - this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe( - () => { - this.notifier.success($localize`Playlist ${videoPlaylistUpdate.displayName} updated.`) - this.router.navigate([ '/my-account', 'video-playlists' ]) - }, - - err => this.error = err.message - ) - } - - isCreation () { - return false - } - - getFormButtonTitle () { - return $localize`Update` - } - - private hydrateFormFromPlaylist () { - this.form.patchValue({ - displayName: this.videoPlaylistToUpdate.displayName, - privacy: this.videoPlaylistToUpdate.privacy.id, - description: this.videoPlaylistToUpdate.description, - videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null - }) - - fetch(this.videoPlaylistToUpdate.thumbnailUrl) - .then(response => response.blob()) - .then(data => { - this.form.patchValue({ - thumbnailfile: data - }) - }) - } -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html deleted file mode 100644 index afcf6a084..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

- - - My playlists {{ pagination.totalItems }} - -

- -
-
- - - Clear filters -
- - - - Create playlist - -
- -
-
-
- -
- -
- - - -
-
-
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss deleted file mode 100644 index 2b7c88246..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss +++ /dev/null @@ -1,78 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.create-button { - @include create-button; -} - -input[type=text] { - @include peertube-input-text(300px); -} - -::ng-deep .action-button { - &.action-button-delete { - margin-right: 10px; - } -} - -.video-playlist { - @include row-blocks; - - .miniature-wrapper { - flex-grow: 1; - - ::ng-deep .miniature { - display: flex; - - .miniature-info { - margin-left: 10px; - width: auto; - } - } - } - - .video-playlist-buttons { - min-width: 190px; - height: max-content; - } -} - -.video-playlists-header { - margin-bottom: 30px; -} - -@media screen and (max-width: $small-view) { - .video-playlists-header { - text-align: center; - } - - .video-playlist { - - .video-playlist-buttons { - margin-top: 10px; - } - } - - my-video-playlist-miniature ::ng-deep .miniature { - flex-direction: column; - - .miniature-info { - margin-left: 0 !important; - } - - .miniature-name { - max-width: $video-thumbnail-width; - } - } -} - -@media screen and (max-width: $mobile-view) { - .video-playlists-header { - flex-direction: column; - - input[type=text] { - width: 100% !important; - margin-bottom: 12px; - } - } -} diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts deleted file mode 100644 index 1e569c0b6..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Subject } from 'rxjs' -import { debounceTime, mergeMap } from 'rxjs/operators' -import { Component, OnInit } from '@angular/core' -import { AuthService, ComponentPagination, ConfirmService, Notifier, User } from '@app/core' -import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { VideoPlaylistType } from '@shared/models' - -@Component({ - selector: 'my-account-video-playlists', - templateUrl: './my-account-video-playlists.component.html', - styleUrls: [ './my-account-video-playlists.component.scss' ] -}) -export class MyAccountVideoPlaylistsComponent implements OnInit { - videoPlaylistsSearch: string - videoPlaylists: VideoPlaylist[] = [] - videoPlaylistSearchChanged = new Subject() - - pagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 5, - totalItems: null - } - - onDataSubject = new Subject() - - private user: User - - constructor ( - private authService: AuthService, - private notifier: Notifier, - private confirmService: ConfirmService, - private videoPlaylistService: VideoPlaylistService - ) {} - - ngOnInit () { - this.user = this.authService.getUser() - - this.loadVideoPlaylists() - - this.videoPlaylistSearchChanged - .pipe( - debounceTime(500)) - .subscribe(() => { - this.loadVideoPlaylists(true) - }) - } - - async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) { - const res = await this.confirmService.confirm( - $localize`Do you really want to delete ${videoPlaylist.displayName}?`, - $localize`Delete` - ) - if (res === false) return - - this.videoPlaylistService.removeVideoPlaylist(videoPlaylist) - .subscribe( - () => { - this.videoPlaylists = this.videoPlaylists - .filter(p => p.id !== videoPlaylist.id) - - this.notifier.success($localize`Playlist ${videoPlaylist.displayName}} deleted.`) - }, - - error => this.notifier.error(error.message) - ) - } - - isRegularPlaylist (playlist: VideoPlaylist) { - return playlist.type.id === VideoPlaylistType.REGULAR - } - - onNearOfBottom () { - // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return - - this.pagination.currentPage += 1 - this.loadVideoPlaylists() - } - - resetSearch () { - this.videoPlaylistsSearch = '' - this.onVideoPlaylistSearchChanged() - } - - onVideoPlaylistSearchChanged () { - this.videoPlaylistSearchChanged.next() - } - - private loadVideoPlaylists (reset = false) { - this.authService.userInformationLoaded - .pipe(mergeMap(() => { - return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt', this.videoPlaylistsSearch) - })) - .subscribe(res => { - if (reset) this.videoPlaylists = [] - this.videoPlaylists = this.videoPlaylists.concat(res.data) - this.pagination.totalItems = res.total - - this.onDataSubject.next(res.data) - }) - } -} diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html deleted file mode 100644 index c7c5a0b69..000000000 --- a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss deleted file mode 100644 index a79fec179..000000000 --- a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -p-autocomplete { - display: block; -} - -.form-group { - margin: 20px 0; -} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts deleted file mode 100644 index 84237dee1..000000000 --- a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' -import { Notifier, UserService } from '@app/core' -import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' -import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { Video, VideoOwnershipService } from '@app/shared/shared-main' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' - -@Component({ - selector: 'my-video-change-ownership', - templateUrl: './video-change-ownership.component.html', - styleUrls: [ './video-change-ownership.component.scss' ] -}) -export class VideoChangeOwnershipComponent extends FormReactive implements OnInit { - @ViewChild('modal', { static: true }) modal: ElementRef - - usernamePropositions: string[] - - error: string = null - - private video: Video | undefined = undefined - - constructor ( - protected formValidatorService: FormValidatorService, - private videoOwnershipService: VideoOwnershipService, - private notifier: Notifier, - private userService: UserService, - private modalService: NgbModal - ) { - super() - } - - ngOnInit () { - this.buildForm({ - username: OWNERSHIP_CHANGE_USERNAME_VALIDATOR - }) - this.usernamePropositions = [] - } - - show (video: Video) { - this.video = video - this.modalService - .open(this.modal, { centered: true }) - .result - .then(() => this.changeOwnership()) - .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing - } - - search (event: { query: string }) { - const query = event.query - this.userService.autocomplete(query) - .subscribe( - usernames => this.usernamePropositions = usernames, - - err => this.notifier.error(err.message) - ) - } - - changeOwnership () { - const username = this.form.value['username'] - - this.videoOwnershipService - .changeOwnership(this.video.id, username) - .subscribe( - () => this.notifier.success($localize`Ownership change request sent.`), - - err => this.notifier.error(err.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 deleted file mode 100644 index aa5b284e7..000000000 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ /dev/null @@ -1,46 +0,0 @@ -

- - - My videos - {{ pagination.totalItems }} - -

- -
-
- - - Clear filters -
-
- - - - - - 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 deleted file mode 100644 index 246f46320..000000000 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ /dev/null @@ -1,127 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -input[type=text] { - @include peertube-input-text(300px); -} - -.action-button-delete-selection { - display: inline-block; - - @include peertube-button; - @include orange-button; - @include button-with-icon(21px); - - my-global-icon { - @include apply-svg-color(#fff); - } -} - -::ng-deep { - .video { - flex-wrap: wrap; - } - - .action-button span { - white-space: nowrap; - } - - .video-miniature { - &.display-as-row { - // width: min-content !important; - width: 100% !important; - - .video-bottom .video-miniature-information { - width: max-content !important; - min-width: unset !important; - } - } - - .video-bottom { - max-width: 350px; - } - } -} - -.action-button { - display: flex; - margin-left: 55px; - margin-top: 10px; - align-self: flex-end; -} - -my-delete-button, -my-edit-button { - margin-right: 10px; -} - -@media screen and (max-width: $small-view) { - .action-button { - flex-direction: column; - align-self: center; - margin-left: 0px; - } - - ::ng-deep { - .video-miniature { - align-items: center; - - .video-bottom, - .video-bottom .video-miniature-information { - /* same width than a.video-thumbnail */ - max-width: $video-thumbnail-width !important; - } - } - } - - my-delete-button, - my-edit-button { - margin-right: 0px; - - ::ng-deep { - span, a { - margin-right: 0px; - } - } - } - - my-delete-button, - my-edit-button, - my-button { - margin-top: 15px; - width: 100%; - text-align: center; - - ::ng-deep { - .action-button { - /* same width than a.video-thumbnail */ - width: $video-thumbnail-width; - } - } - } -} - -// Adapt my-video-miniature on small screens with menu -@media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) { - :host-context(.main-col:not(.expanded)) { - ::ng-deep { - .video-miniature { - flex-direction: column; - - .video-miniature-name { - max-width: $video-thumbnail-width; - } - } - } - } -} - -@media screen and (max-width: $mobile-view) { - .videos-header { - flex-direction: column; - - input[type=text] { - width: 100% !important; - } - } -} 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 deleted file mode 100644 index 84f022ad2..000000000 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { concat, Observable, Subject } from 'rxjs' -import { debounceTime, tap, toArray } from 'rxjs/operators' -import { Component, OnInit, ViewChild } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' -import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' -import { immutableAssign } from '@app/helpers' -import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' -import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' -import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' -import { VideoSortField } from '@shared/models' -import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' - -@Component({ - selector: 'my-account-videos', - templateUrl: './my-account-videos.component.html', - styleUrls: [ './my-account-videos.component.scss' ] -}) -export class MyAccountVideosComponent implements OnInit, DisableForReuseHook { - @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent - @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent - @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent - - titlePage: string - selection: SelectionType = {} - pagination: ComponentPagination = { - currentPage: 1, - itemsPerPage: 10, - totalItems: null - } - miniatureDisplayOptions: MiniatureDisplayOptions = { - date: true, - views: true, - by: true, - privacyLabel: false, - privacyText: true, - state: true, - blacklistInfo: true - } - ownerDisplayType: OwnerDisplayType = 'videoChannel' - - videoActions: DropdownAction<{ video: Video }>[] = [] - - videos: Video[] = [] - videosSearch: string - videosSearchChanged = new Subject() - getVideosObservableFunction = this.getVideosObservable.bind(this) - - constructor ( - protected router: Router, - protected serverService: ServerService, - protected route: ActivatedRoute, - protected authService: AuthService, - protected notifier: Notifier, - protected screenService: ScreenService, - private confirmService: ConfirmService, - private videoService: VideoService - ) { - this.titlePage = $localize`My videos` - } - - ngOnInit () { - this.buildActions() - - this.videosSearchChanged - .pipe(debounceTime(500)) - .subscribe(() => { - this.videosSelection.reloadVideos() - }) - } - - resetSearch () { - this.videosSearch = '' - this.onVideosSearchChanged() - } - - onVideosSearchChanged () { - this.videosSearchChanged.next() - } - - disableForReuse () { - this.videosSelection.disableForReuse() - } - - enabledForReuse () { - this.videosSelection.enabledForReuse() - } - - getVideosObservable (page: number, sort: VideoSortField) { - const newPagination = immutableAssign(this.pagination, { currentPage: page }) - - return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) - .pipe( - tap(res => this.pagination.totalItems = res.total) - ) - } - - async deleteSelectedVideos () { - const toDeleteVideosIds = Object.keys(this.selection) - .filter(k => this.selection[ k ] === true) - .map(k => parseInt(k, 10)) - - const res = await this.confirmService.confirm( - $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`, - $localize`Delete` - ) - if (res === false) return - - const observables: Observable[] = [] - for (const videoId of toDeleteVideosIds) { - const o = this.videoService.removeVideo(videoId) - .pipe(tap(() => this.removeVideoFromArray(videoId))) - - observables.push(o) - } - - concat(...observables) - .pipe(toArray()) - .subscribe( - () => { - this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`) - this.selection = {} - }, - - err => this.notifier.error(err.message) - ) - } - - async deleteVideo (video: Video) { - const res = await this.confirmService.confirm( - $localize`Do you really want to delete ${video.name}?`, - $localize`Delete` - ) - if (res === false) return - - this.videoService.removeVideo(video.id) - .subscribe( - () => { - this.notifier.success($localize`Video ${video.name} deleted.`) - this.removeVideoFromArray(video.id) - }, - - error => this.notifier.error(error.message) - ) - } - - changeOwnership (video: Video) { - this.videoChangeOwnershipModal.show(video) - } - - displayLiveInformation (video: Video) { - this.liveStreamInformationModal.show(video) - } - - private removeVideoFromArray (id: number) { - this.videos = this.videos.filter(v => v.id !== id) - } - - private buildActions () { - this.videoActions = [ - { - label: $localize`Display live information`, - handler: ({ video }) => this.displayLiveInformation(video), - isDisplayed: ({ video }) => video.isLive, - iconName: 'live' - }, - { - label: $localize`Change ownership`, - handler: ({ video }) => this.changeOwnership(video), - iconName: 'ownership-change' - }, - { - label: $localize`Delete`, - handler: ({ video }) => this.deleteVideo(video), - iconName: 'delete' - } - ] - } -} diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index d3bf8d143..d6e9d1c15 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from '@angular/core' -import { AuthService, AuthUser, ScreenService, ServerService } from '@app/core' -import { ServerConfig } from '@shared/models' +import { AuthUser, ScreenService } from '@app/core' import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component' @Component({ @@ -12,11 +11,7 @@ export class MyAccountComponent implements OnInit { menuEntries: TopMenuDropdownParam[] = [] user: AuthUser - private serverConfig: ServerConfig - constructor ( - private serverService: ServerService, - private authService: AuthService, private screenService: ScreenService ) { } @@ -25,67 +20,12 @@ export class MyAccountComponent implements OnInit { } ngOnInit (): void { - this.serverConfig = this.serverService.getTmpConfig() - this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) - - this.user = this.authService.getUser() - - this.authService.userInformationLoaded.subscribe( - () => this.buildMenu() - ) - } - - isVideoImportEnabled () { - const importConfig = this.serverConfig.import.videos - - return importConfig.http.enabled || importConfig.torrent.enabled + this.buildMenu() } private buildMenu () { - const libraryEntries: TopMenuDropdownParam = { - label: $localize`My library`, - children: [ - { - label: $localize`My channels`, - routerLink: '/my-account/video-channels', - iconName: 'channel' - }, - { - label: $localize`My videos`, - routerLink: '/my-account/videos', - iconName: 'videos', - isDisplayed: () => this.user.canSeeVideosLink - }, - { - label: $localize`My playlists`, - routerLink: '/my-account/video-playlists', - iconName: 'playlists' - }, - { - label: $localize`My subscriptions`, - routerLink: '/my-account/subscriptions', - iconName: 'subscriptions' - }, - { - label: $localize`My history`, - routerLink: '/my-account/history/videos', - iconName: 'history' - } - ] - } - - if (this.isVideoImportEnabled()) { - libraryEntries.children.push({ - label: 'My imports', - routerLink: '/my-account/video-imports', - iconName: 'cloud-download', - isDisplayed: () => this.user.canSeeVideosLink - }) - } - - const miscEntries: TopMenuDropdownParam = { - label: $localize`Misc`, + const moderationEntries: TopMenuDropdownParam = { + label: $localize`Moderation`, children: [ { label: $localize`Muted accounts`, @@ -98,29 +38,25 @@ export class MyAccountComponent implements OnInit { iconName: 'peertube-x' }, { - label: $localize`My abuse reports`, + label: $localize`Abuse reports`, routerLink: '/my-account/abuses', iconName: 'flag' - }, - { - label: $localize`Ownership changes`, - routerLink: '/my-account/ownership', - iconName: 'download' } ] } this.menuEntries = [ { - label: $localize`My settings`, + label: $localize`Settings`, routerLink: '/my-account/settings' }, + { - label: $localize`My notifications`, + label: $localize`Notifications`, routerLink: '/my-account/notifications' }, - libraryEntries, - miscEntries + + moderationEntries ] } } diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 6d21fff72..d3b6a9fa3 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -6,21 +6,14 @@ import { NgModule } from '@angular/core' import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' import { SharedFormModule } from '@app/shared/shared-forms' import { SharedGlobalIconModule } from '@app/shared/shared-icons' -import { SharedVideoLiveModule } from '@app/shared/shared-video-live' import { SharedMainModule } from '@app/shared/shared-main' import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedShareModal } from '@app/shared/shared-share-modal' import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' -import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module' -import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' -import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' -import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component' import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' -import { MyAccountAcceptOwnershipComponent } from './my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component' -import { MyAccountOwnershipComponent } from './my-account-ownership/my-account-ownership.component' import { MyAccountRoutingModule } from './my-account-routing.module' import { MyAccountChangeEmailComponent } from './my-account-settings/my-account-change-email' import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' @@ -28,14 +21,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' -import { MyAccountSubscriptionsComponent } from './my-account-subscriptions/my-account-subscriptions.component' -import { MyAccountVideoImportsComponent } from './my-account-video-imports/my-account-video-imports.component' -import { MyAccountVideoPlaylistCreateComponent } from './my-account-video-playlists/my-account-video-playlist-create.component' -import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-playlists/my-account-video-playlist-elements.component' -import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' -import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' -import { VideoChangeOwnershipComponent } from './my-account-videos/modals/video-change-ownership.component' -import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' import { MyAccountComponent } from './my-account.component' @NgModule({ @@ -50,14 +35,10 @@ import { MyAccountComponent } from './my-account.component' SharedMainModule, SharedFormModule, SharedModerationModule, - SharedVideoMiniatureModule, - SharedUserSubscriptionModule, - SharedVideoPlaylistModule, SharedUserInterfaceSettingsModule, SharedGlobalIconModule, SharedAbuseListModule, - SharedShareModal, - SharedVideoLiveModule + SharedShareModal ], declarations: [ @@ -67,26 +48,12 @@ import { MyAccountComponent } from './my-account.component' MyAccountProfileComponent, MyAccountChangeEmailComponent, - MyAccountVideosComponent, - - VideoChangeOwnershipComponent, - - MyAccountOwnershipComponent, - MyAccountAcceptOwnershipComponent, - MyAccountVideoImportsComponent, MyAccountDangerZoneComponent, - MyAccountSubscriptionsComponent, MyAccountBlocklistComponent, MyAccountAbusesListComponent, MyAccountServerBlocklistComponent, - MyAccountHistoryComponent, MyAccountNotificationsComponent, - MyAccountNotificationPreferencesComponent, - - MyAccountVideoPlaylistCreateComponent, - MyAccountVideoPlaylistUpdateComponent, - MyAccountVideoPlaylistsComponent, - MyAccountVideoPlaylistElementsComponent + MyAccountNotificationPreferencesComponent ], exports: [ diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts new file mode 100644 index 000000000..1d0cbf246 --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts @@ -0,0 +1,78 @@ +import { Component, OnInit } from '@angular/core' +import { Router } from '@angular/router' +import { AuthService, Notifier } from '@app/core' +import { + VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, + VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, + VIDEO_CHANNEL_NAME_VALIDATOR, + VIDEO_CHANNEL_SUPPORT_VALIDATOR +} from '@app/shared/form-validators/video-channel-validators' +import { FormValidatorService } from '@app/shared/shared-forms' +import { VideoChannelService } from '@app/shared/shared-main' +import { VideoChannelCreate } from '@shared/models' +import { MyVideoChannelEdit } from './my-video-channel-edit' + +@Component({ + templateUrl: './my-video-channel-edit.component.html', + styleUrls: [ './my-video-channel-edit.component.scss' ] +}) +export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { + error: string + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private notifier: Notifier, + private router: Router, + private videoChannelService: VideoChannelService + ) { + super() + } + + ngOnInit () { + this.buildForm({ + name: VIDEO_CHANNEL_NAME_VALIDATOR, + 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, + description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, + support: VIDEO_CHANNEL_SUPPORT_VALIDATOR + }) + } + + formValidated () { + this.error = undefined + + const body = this.form.value + const videoChannelCreate: VideoChannelCreate = { + name: body.name, + displayName: body['display-name'], + description: body.description || null, + support: body.support || null + } + + this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( + () => { + this.authService.refreshUserInformation() + + this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) + this.router.navigate([ '/my-library', 'video-channels' ]) + }, + + err => { + if (err.status === 409) { + this.error = $localize`This name already exists on this instance.` + return + } + + this.error = err.message + } + ) + } + + isCreation () { + return true + } + + getFormButtonTitle () { + return $localize`Create` + } +} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html new file mode 100644 index 000000000..7e0c4e732 --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html @@ -0,0 +1,105 @@ + + +
{{ error }}
+ +
+ +
+
+
NEW CHANNEL
+
CHANNEL
+
+ +
+ +
+ +
+ +
+ @{{ instanceHost }} +
+
+
+ {{ formErrors['name'] }} +
+
+ + + +
+ + +
+ {{ formErrors['display-name'] }} +
+
+ +
+ + +
+ {{ formErrors.description }} +
+
+ +
+ + + +
+ {{ formErrors.support }} +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss new file mode 100644 index 000000000..8f8af655c --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss @@ -0,0 +1,67 @@ +@import '_variables'; +@import '_mixins'; + +label { + font-weight: $font-regular; + font-size: 100%; +} + +.video-channel-title { + @include settings-big-title; +} + +my-actor-avatar-info { + display: block; + margin-bottom: 20px; +} + +.input-group { + @include peertube-input-group(fit-content); +} + +.input-group-append { + height: 30px; +} + +input { + &[type=text] { + @include peertube-input-text(340px); + + display: block; + + &#name { + width: auto; + flex-grow: 1; + } + } + + &[type=submit] { + @include peertube-button; + @include orange-button; + margin-left: auto; + } +} + +textarea { + @include peertube-textarea(500px, 150px); + + display: block; +} + +.peertube-select-container { + @include peertube-select-container(340px); +} + +.breadcrumb { + @include breadcrumb; +} + +@media screen and (max-width: $small-view) { + input[type=text]#name { + width: auto !important; + } + + label[for=name] + div, textarea { + width: 100%; + } +} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts new file mode 100644 index 000000000..09db0df9d --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts @@ -0,0 +1,22 @@ +import { FormReactive } from '@app/shared/shared-forms' +import { VideoChannel } from '@app/shared/shared-main' + +export abstract class MyVideoChannelEdit extends FormReactive { + // We need it even in the create component because it's used in the edit template + videoChannelToUpdate: VideoChannel + + abstract isCreation (): boolean + abstract getFormButtonTitle (): string + + get instanceHost () { + return window.location.host + } + + // We need this method so angular does not complain in child template that doesn't need this + onAvatarChange (formData: FormData) { /* empty */ } + + // Should be implemented by the child + isBulkUpdateVideosDisplayed () { + return false + } +} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts new file mode 100644 index 000000000..c6cb5ade6 --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts @@ -0,0 +1,135 @@ +import { Subscription } from 'rxjs' +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { AuthService, Notifier, ServerService } from '@app/core' +import { + VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, + VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, + VIDEO_CHANNEL_SUPPORT_VALIDATOR +} from '@app/shared/form-validators/video-channel-validators' +import { FormValidatorService } from '@app/shared/shared-forms' +import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' +import { ServerConfig, VideoChannelUpdate } from '@shared/models' +import { MyVideoChannelEdit } from './my-video-channel-edit' + +@Component({ + selector: 'my-video-channel-update', + templateUrl: './my-video-channel-edit.component.html', + styleUrls: [ './my-video-channel-edit.component.scss' ] +}) +export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { + error: string + videoChannelToUpdate: VideoChannel + + private paramsSub: Subscription + private oldSupportField: string + private serverConfig: ServerConfig + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private notifier: Notifier, + private router: Router, + private route: ActivatedRoute, + private videoChannelService: VideoChannelService, + private serverService: ServerService + ) { + super() + } + + ngOnInit () { + this.serverConfig = this.serverService.getTmpConfig() + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.buildForm({ + 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, + description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, + support: VIDEO_CHANNEL_SUPPORT_VALIDATOR, + bulkVideosSupportUpdate: null + }) + + this.paramsSub = this.route.params.subscribe(routeParams => { + const videoChannelId = routeParams['videoChannelId'] + + this.videoChannelService.getVideoChannel(videoChannelId).subscribe( + videoChannelToUpdate => { + this.videoChannelToUpdate = videoChannelToUpdate + + this.oldSupportField = videoChannelToUpdate.support + + this.form.patchValue({ + 'display-name': videoChannelToUpdate.displayName, + description: videoChannelToUpdate.description, + support: videoChannelToUpdate.support + }) + }, + + err => this.error = err.message + ) + }) + } + + ngOnDestroy () { + if (this.paramsSub) this.paramsSub.unsubscribe() + } + + formValidated () { + this.error = undefined + + const body = this.form.value + const videoChannelUpdate: VideoChannelUpdate = { + displayName: body['display-name'], + description: body.description || null, + support: body.support || null, + bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false + } + + this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( + () => { + this.authService.refreshUserInformation() + + this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`) + + this.router.navigate([ '/my-library', 'video-channels' ]) + }, + + err => this.error = err.message + ) + } + + onAvatarChange (formData: FormData) { + this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) + .subscribe( + data => { + this.notifier.success($localize`Avatar changed.`) + + this.videoChannelToUpdate.updateAvatar(data.avatar) + }, + + err => this.notifier.error(err.message) + ) + } + + get maxAvatarSize () { + return this.serverConfig.avatar.file.size.max + } + + get avatarExtensions () { + return this.serverConfig.avatar.file.extensions.join(',') + } + + isCreation () { + return false + } + + getFormButtonTitle () { + return $localize`Update` + } + + isBulkUpdateVideosDisplayed () { + if (this.oldSupportField === undefined) return false + + return this.oldSupportField !== this.form.value['support'] + } +} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts new file mode 100644 index 000000000..6b8efad0b --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts @@ -0,0 +1,41 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component' +import { MyVideoChannelCreateComponent } from './my-video-channel-create.component' +import { MyVideoChannelsComponent } from './my-video-channels.component' + +const myVideoChannelsRoutes: Routes = [ + { + path: '', + component: MyVideoChannelsComponent, + data: { + meta: { + title: $localize`My video channels` + } + } + }, + { + path: 'create', + component: MyVideoChannelCreateComponent, + data: { + meta: { + title: $localize`Create a new video channel` + } + } + }, + { + path: 'update/:videoChannelId', + component: MyVideoChannelUpdateComponent, + data: { + meta: { + title: $localize`Update video channel` + } + } + } +] + +@NgModule({ + imports: [ RouterModule.forChild(myVideoChannelsRoutes) ], + exports: [ RouterModule ] +}) +export class MyVideoChannelsRoutingModule {} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html new file mode 100644 index 000000000..205d23cd5 --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html @@ -0,0 +1,49 @@ +

+ + + My channels + {{ totalItems }} + +

+ +
+
+ + + Clear filters +
+ + + + Create video channel + +
+ +
+
+ + Avatar + + +
+ +
{{ videoChannel.displayName }}
+
{{ videoChannel.nameWithHost }}
+
+ +
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
+ +
{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}
+ +
+ + +
+ +
+ +
+
+
+
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss new file mode 100644 index 000000000..f2f42459f --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss @@ -0,0 +1,125 @@ +@import '_variables'; +@import '_mixins'; + +.create-button { + @include create-button; +} + +input[type=text] { + @include peertube-input-text(300px); +} + +::ng-deep .action-button { + &.action-button-edit { + margin-right: 10px; + } +} + +.video-channel { + @include row-blocks; + padding-bottom: 0; + + img { + @include avatar(80px); + + margin-right: 10px; + } + + .video-channel-info { + flex-grow: 1; + + a.video-channel-names { + @include disable-default-a-behaviour; + + width: fit-content; + display: flex; + align-items: baseline; + color: pvar(--mainForegroundColor); + + .video-channel-display-name { + font-weight: $font-semibold; + font-size: 18px; + } + + .video-channel-name { + font-size: 14px; + color: $grey-actor-name; + margin-left: 5px; + } + } + } + + .video-channel-buttons { + margin-top: 10px; + min-width: 190px; + } +} + +::ng-deep .chartjs-render-monitor { + position: relative; + top: 1px; +} + +.video-channels-header { + margin-bottom: 30px; +} + +@media screen and (max-width: $small-view) { + .video-channels-header { + text-align: center; + } + + .video-channel { + padding-bottom: 10px; + + .video-channel-info { + padding-bottom: 10px; + text-align: center; + + .video-channel-names { + flex-direction: column; + align-items: center !important; + margin: auto; + + .video-channel-name { + margin-left: 0px !important; + } + } + } + + img { + margin-right: 0; + } + + .video-channel-buttons { + align-self: center; + } + } +} + +@media screen and (max-width: $mobile-view) { + .video-channels-header { + flex-direction: column; + + input[type=text] { + width: 100% !important; + margin-bottom: 12px; + } + } +} + +@media screen and (min-width: breakpoint(lg)) { + :host-context(.main-col:not(.expanded)) { + .video-channel-buttons { + float: right; + } + } +} + +@media screen and (min-width: $small-view) { + :host-context(.expanded) { + .video-channel-buttons { + float: right; + } + } +} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts new file mode 100644 index 000000000..a63e98a51 --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts @@ -0,0 +1,171 @@ +import { ChartData } from 'chart.js' +import { max, maxBy, min, minBy } from 'lodash-es' +import { Subject } from 'rxjs' +import { debounceTime, mergeMap } from 'rxjs/operators' +import { Component, OnInit } from '@angular/core' +import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' +import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' + +@Component({ + templateUrl: './my-video-channels.component.html', + styleUrls: [ './my-video-channels.component.scss' ] +}) +export class MyVideoChannelsComponent implements OnInit { + totalItems: number + + videoChannels: VideoChannel[] = [] + videoChannelsChartData: ChartData[] + videoChannelsMinimumDailyViews = 0 + videoChannelsMaximumDailyViews: number + + channelsSearch: string + channelsSearchChanged = new Subject() + + private user: User + + constructor ( + private authService: AuthService, + private notifier: Notifier, + private confirmService: ConfirmService, + private videoChannelService: VideoChannelService, + private screenService: ScreenService + ) {} + + ngOnInit () { + this.user = this.authService.getUser() + + this.loadVideoChannels() + + this.channelsSearchChanged + .pipe(debounceTime(500)) + .subscribe(() => { + this.loadVideoChannels() + }) + } + + get isInSmallView () { + return this.screenService.isInSmallView() + } + + get chartOptions () { + return { + legend: { + display: false + }, + scales: { + xAxes: [{ + display: false + }], + yAxes: [{ + display: false, + ticks: { + min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)), + max: Math.max(1, this.videoChannelsMaximumDailyViews) + } + }] + }, + layout: { + padding: { + left: 15, + right: 15, + top: 10, + bottom: 0 + } + }, + elements: { + point: { + radius: 0 + } + }, + tooltips: { + mode: 'index', + intersect: false, + custom: function (tooltip: any) { + if (!tooltip) return + // disable displaying the color box + tooltip.displayColors = false + }, + callbacks: { + label: (tooltip: any, data: any) => `${tooltip.value} views` + } + }, + hover: { + mode: 'index', + intersect: false + } + } + } + + resetSearch () { + this.channelsSearch = '' + this.onChannelsSearchChanged() + } + + onChannelsSearchChanged () { + this.channelsSearchChanged.next() + } + + async deleteVideoChannel (videoChannel: VideoChannel) { + const res = await this.confirmService.confirmWithInput( + $localize`Do you really want to delete ${videoChannel.displayName}? +It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another +channel with the same name (${videoChannel.name})!`, + + $localize`Please type the display name of the video channel (${videoChannel.displayName}) to confirm`, + + videoChannel.displayName, + + $localize`Delete` + ) + if (res === false) return + + this.videoChannelService.removeVideoChannel(videoChannel) + .subscribe( + () => { + this.loadVideoChannels() + this.notifier.success($localize`Video channel ${videoChannel.displayName} deleted.`) + }, + + error => this.notifier.error(error.message) + ) + } + + private loadVideoChannels () { + this.authService.userInformationLoaded + .pipe(mergeMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch))) + .subscribe(res => { + this.videoChannels = res.data + this.totalItems = res.total + + // chart data + this.videoChannelsChartData = this.videoChannels.map(v => ({ + labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), + datasets: [ + { + label: $localize`Views for the day`, + data: v.viewsPerDay.map(day => day.views), + fill: false, + borderColor: '#c6c6c6' + } + ] + } as ChartData)) + + // chart options that depend on chart data: + // we don't want to skew values and have min at 0, so we define what the floor/ceiling is here + this.videoChannelsMinimumDailyViews = min( + // compute local minimum daily views for each channel, by their "views" attribute + this.videoChannels.map(v => minBy( + v.viewsPerDay, + day => day.views + ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute + ) + this.videoChannelsMaximumDailyViews = max( + // compute local maximum daily views for each channel, by their "views" attribute + this.videoChannels.map(v => maxBy( + v.viewsPerDay, + day => day.views + ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute + ) + }) + } +} diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts new file mode 100644 index 000000000..92b56db49 --- /dev/null +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts @@ -0,0 +1,31 @@ +import { ChartModule } from 'primeng/chart' +import { NgModule } from '@angular/core' +import { SharedFormModule } from '@app/shared/shared-forms' +import { SharedGlobalIconModule } from '@app/shared/shared-icons' +import { SharedMainModule } from '@app/shared/shared-main' +import { MyVideoChannelCreateComponent } from './my-video-channel-create.component' +import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component' +import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module' +import { MyVideoChannelsComponent } from './my-video-channels.component' + +@NgModule({ + imports: [ + MyVideoChannelsRoutingModule, + + ChartModule, + + SharedMainModule, + SharedFormModule, + SharedGlobalIconModule + ], + + declarations: [ + MyVideoChannelsComponent, + MyVideoChannelCreateComponent, + MyVideoChannelUpdateComponent + ], + + exports: [], + providers: [] +}) +export class MyVideoChannelsModule { } diff --git a/client/src/app/+my-library/index.ts b/client/src/app/+my-library/index.ts new file mode 100644 index 000000000..d51a36a7c --- /dev/null +++ b/client/src/app/+my-library/index.ts @@ -0,0 +1,3 @@ +export * from './my-library-routing.module' +export * from './my-library.component' +export * from './my-library.module' diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html new file mode 100644 index 000000000..cff46a41d --- /dev/null +++ b/client/src/app/+my-library/my-history/my-history.component.html @@ -0,0 +1,28 @@ +

+ + My history +

+ +
+
+ + +
+ + +
+ + +
You don't have any video history yet.
+ +
+
+ +
+
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss new file mode 100644 index 000000000..9eeeaf310 --- /dev/null +++ b/client/src/app/+my-library/my-history/my-history.component.scss @@ -0,0 +1,59 @@ +@import '_variables'; +@import '_mixins'; + +.no-history { + display: flex; + justify-content: center; + margin-top: 50px; + font-weight: $font-semibold; + font-size: 16px; +} + +.top-buttons { + margin-bottom: 20px; + display: flex; + align-items: center; + flex-wrap: wrap; + + .history-switch { + display: flex; + flex-grow: 1; + + label { + margin: 0 0 0 5px; + } + } + + .delete-history { + @include peertube-button; + @include grey-button; + @include button-with-icon; + + font-size: 15px; + } +} + +.video { + @include row-blocks; + + .my-video-miniature { + flex-grow: 1; + } +} + +@media screen and (max-width: $mobile-view) { + .top-buttons { + .history-switch label, .delete-history { + @include ellipsis; + } + + .history-switch label { + width: 60%; + } + + .delete-history { + margin-left: auto; + max-width: 32%; + } + } +} diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts new file mode 100644 index 000000000..e11f05c47 --- /dev/null +++ b/client/src/app/+my-library/my-history/my-history.component.ts @@ -0,0 +1,102 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { + AuthService, + ComponentPagination, + ConfirmService, + LocalStorageService, + Notifier, + ScreenService, + ServerService, + UserService +} from '@app/core' +import { immutableAssign } from '@app/helpers' +import { UserHistoryService } from '@app/shared/shared-main' +import { AbstractVideoList } from '@app/shared/shared-video-miniature' + +@Component({ + templateUrl: './my-history.component.html', + styleUrls: [ './my-history.component.scss' ] +}) +export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { + titlePage: string + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 5, + totalItems: null + } + videosHistoryEnabled: boolean + + constructor ( + protected router: Router, + protected serverService: ServerService, + protected route: ActivatedRoute, + protected authService: AuthService, + protected userService: UserService, + protected notifier: Notifier, + protected screenService: ScreenService, + protected storageService: LocalStorageService, + private confirmService: ConfirmService, + private userHistoryService: UserHistoryService + ) { + super() + + this.titlePage = $localize`My videos history` + } + + ngOnInit () { + super.ngOnInit() + + this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled + } + + ngOnDestroy () { + super.ngOnDestroy() + } + + getVideosObservable (page: number) { + const newPagination = immutableAssign(this.pagination, { currentPage: page }) + + return this.userHistoryService.getUserVideosHistory(newPagination) + } + + generateSyndicationList () { + throw new Error('Method not implemented.') + } + + onVideosHistoryChange () { + this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled }) + .subscribe( + () => { + const message = this.videosHistoryEnabled === true ? + $localize`Videos history is enabled` : + $localize`Videos history is disabled` + + this.notifier.success(message) + + this.authService.refreshUserInformation() + }, + + err => this.notifier.error(err.message) + ) + } + + async deleteHistory () { + const title = $localize`Delete videos history` + const message = $localize`Are you sure you want to delete all your videos history?` + + const res = await this.confirmService.confirm(message, title) + if (res !== true) return + + this.userHistoryService.deleteUserVideosHistory() + .subscribe( + () => { + this.notifier.success($localize`Videos history deleted`) + + this.reloadVideos() + }, + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/+my-library/my-library-routing.module.ts b/client/src/app/+my-library/my-library-routing.module.ts new file mode 100644 index 000000000..d8e5aa562 --- /dev/null +++ b/client/src/app/+my-library/my-library-routing.module.ts @@ -0,0 +1,134 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { MetaGuard } from '@ngx-meta/core' +import { LoginGuard } from '../core' +import { MyHistoryComponent } from './my-history/my-history.component' +import { MyLibraryComponent } from './my-library.component' +import { MyOwnershipComponent } from './my-ownership/my-ownership.component' +import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component' +import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' +import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' +import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' +import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-playlist-update.component' +import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component' +import { MyVideosComponent } from './my-videos/my-videos.component' + +const myLibraryRoutes: Routes = [ + { + path: '', + component: MyLibraryComponent, + canActivateChild: [ MetaGuard, LoginGuard ], + children: [ + { + path: '', + redirectTo: 'video-channels', + pathMatch: 'full' + }, + + { + path: 'video-channels', + loadChildren: () => { + return import('./+my-video-channels/my-video-channels.module').then(m => m.MyVideoChannelsModule) + } + }, + + { + path: 'video-playlists', + component: MyVideoPlaylistsComponent, + data: { + meta: { + title: $localize`My playlists` + } + } + }, + { + path: 'video-playlists/create', + component: MyVideoPlaylistCreateComponent, + data: { + meta: { + title: $localize`Create a new playlist` + } + } + }, + { + path: 'video-playlists/:videoPlaylistId', + component: MyVideoPlaylistElementsComponent, + data: { + meta: { + title: $localize`Playlist elements` + } + } + }, + { + path: 'video-playlists/update/:videoPlaylistId', + component: MyVideoPlaylistUpdateComponent, + data: { + meta: { + title: $localize`Update playlist` + } + } + }, + + { + path: 'videos', + component: MyVideosComponent, + data: { + meta: { + title: $localize`My videos` + }, + reuse: { + enabled: true, + key: 'my-videos-list' + } + } + }, + { + path: 'video-imports', + component: MyVideoImportsComponent, + data: { + meta: { + title: $localize`My video imports` + } + } + }, + { + path: 'subscriptions', + component: MySubscriptionsComponent, + data: { + meta: { + title: $localize`My subscriptions` + } + } + }, + { + path: 'ownership', + component: MyOwnershipComponent, + data: { + meta: { + title: $localize`Ownership changes` + } + } + }, + + { + path: 'history/videos', + component: MyHistoryComponent, + data: { + meta: { + title: $localize`My video history` + }, + reuse: { + enabled: true, + key: 'my-videos-history-list' + } + } + } + ] + } +] + +@NgModule({ + imports: [ RouterModule.forChild(myLibraryRoutes) ], + exports: [ RouterModule ] +}) +export class MyLibraryRoutingModule {} diff --git a/client/src/app/+my-library/my-library.component.html b/client/src/app/+my-library/my-library.component.html new file mode 100644 index 000000000..b465d0156 --- /dev/null +++ b/client/src/app/+my-library/my-library.component.html @@ -0,0 +1,7 @@ +
+ + +
+ +
+
diff --git a/client/src/app/+my-library/my-library.component.scss b/client/src/app/+my-library/my-library.component.scss new file mode 100644 index 000000000..a5bb499b4 --- /dev/null +++ b/client/src/app/+my-library/my-library.component.scss @@ -0,0 +1,13 @@ +@import '_variables'; +@import '_mixins'; + +.row { + flex-direction: column; + width: 100%; + + & > my-top-menu-dropdown:nth-child(1) { + flex-grow: 1; + } + + @include sub-menu-h1; +} diff --git a/client/src/app/+my-library/my-library.component.ts b/client/src/app/+my-library/my-library.component.ts new file mode 100644 index 000000000..0cc91e484 --- /dev/null +++ b/client/src/app/+my-library/my-library.component.ts @@ -0,0 +1,76 @@ +import { Component, OnInit } from '@angular/core' +import { AuthService, AuthUser, ScreenService, ServerService } from '@app/core' +import { ServerConfig } from '@shared/models' +import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component' + +@Component({ + templateUrl: './my-library.component.html', + styleUrls: [ './my-library.component.scss' ] +}) +export class MyLibraryComponent implements OnInit { + menuEntries: TopMenuDropdownParam[] = [] + user: AuthUser + + private serverConfig: ServerConfig + + constructor ( + private serverService: ServerService, + private authService: AuthService, + private screenService: ScreenService + ) { } + + get isBroadcastMessageDisplayed () { + return this.screenService.isBroadcastMessageDisplayed + } + + ngOnInit (): void { + this.serverConfig = this.serverService.getTmpConfig() + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.user = this.authService.getUser() + + this.authService.userInformationLoaded.subscribe( + () => this.buildMenu() + ) + } + + isVideoImportEnabled () { + const importConfig = this.serverConfig.import.videos + + return importConfig.http.enabled || importConfig.torrent.enabled + } + + private buildMenu () { + this.menuEntries = [ + { + label: $localize`Channels`, + routerLink: '/my-library/video-channels' + } + ] + + if (this.user.canSeeVideosLink) { + this.menuEntries.push({ + label: $localize`Videos`, + routerLink: '/my-library/videos' + }) + } + + this.menuEntries = this.menuEntries.concat([ + { + label: $localize`Playlists`, + routerLink: '/my-library/video-playlists' + }, + + { + label: $localize`Subscriptions`, + routerLink: '/my-library/subscriptions' + }, + + { + label: $localize`History`, + routerLink: '/my-library/history/videos' + } + ]) + } +} diff --git a/client/src/app/+my-library/my-library.module.ts b/client/src/app/+my-library/my-library.module.ts new file mode 100644 index 000000000..bf791952c --- /dev/null +++ b/client/src/app/+my-library/my-library.module.ts @@ -0,0 +1,79 @@ +import { AutoCompleteModule } from 'primeng/autocomplete' +import { InputSwitchModule } from 'primeng/inputswitch' +import { TableModule } from 'primeng/table' +import { DragDropModule } from '@angular/cdk/drag-drop' +import { NgModule } from '@angular/core' +import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' +import { SharedFormModule } from '@app/shared/shared-forms' +import { SharedGlobalIconModule } from '@app/shared/shared-icons' +import { SharedMainModule } from '@app/shared/shared-main' +import { SharedModerationModule } from '@app/shared/shared-moderation' +import { SharedShareModal } from '@app/shared/shared-share-modal' +import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' +import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module' +import { SharedVideoLiveModule } from '@app/shared/shared-video-live' +import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' +import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' +import { MyHistoryComponent } from './my-history/my-history.component' +import { MyLibraryRoutingModule } from './my-library-routing.module' +import { MyLibraryComponent } from './my-library.component' +import { MyAcceptOwnershipComponent } from './my-ownership/my-accept-ownership/my-accept-ownership.component' +import { MyOwnershipComponent } from './my-ownership/my-ownership.component' +import { MySubscriptionsComponent } from './my-subscriptions/my-subscriptions.component' +import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component' +import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component' +import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component' +import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-playlist-update.component' +import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component' +import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component' +import { MyVideosComponent } from './my-videos/my-videos.component' + +@NgModule({ + imports: [ + MyLibraryRoutingModule, + + AutoCompleteModule, + TableModule, + InputSwitchModule, + DragDropModule, + + SharedMainModule, + SharedFormModule, + SharedModerationModule, + SharedVideoMiniatureModule, + SharedUserSubscriptionModule, + SharedVideoPlaylistModule, + SharedUserInterfaceSettingsModule, + SharedGlobalIconModule, + SharedAbuseListModule, + SharedShareModal, + SharedVideoLiveModule + ], + + declarations: [ + MyLibraryComponent, + + MyVideosComponent, + + VideoChangeOwnershipComponent, + + MyOwnershipComponent, + MyAcceptOwnershipComponent, + MyVideoImportsComponent, + MySubscriptionsComponent, + MyHistoryComponent, + + MyVideoPlaylistCreateComponent, + MyVideoPlaylistUpdateComponent, + MyVideoPlaylistsComponent, + MyVideoPlaylistElementsComponent + ], + + exports: [ + MyLibraryComponent + ], + + providers: [] +}) +export class MyLibraryModule { +} diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html new file mode 100644 index 000000000..def1cbab6 --- /dev/null +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html @@ -0,0 +1,36 @@ + + + + + + + diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss new file mode 100644 index 000000000..c7357f62d --- /dev/null +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss @@ -0,0 +1,14 @@ +@import '_variables'; +@import '_mixins'; + +select { + display: block; +} + +.peertube-select-container { + @include peertube-select-container(350px); +} + +.form-group { + margin: 20px 0; +} \ No newline at end of file diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts new file mode 100644 index 000000000..587a455f0 --- /dev/null +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts @@ -0,0 +1,72 @@ +import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { AuthService, Notifier } from '@app/core' +import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' +import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { VideoChangeOwnership, VideoChannel } from '@shared/models' + +@Component({ + selector: 'my-accept-ownership', + templateUrl: './my-accept-ownership.component.html', + styleUrls: [ './my-accept-ownership.component.scss' ] +}) +export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { + @Output() accepted = new EventEmitter() + + @ViewChild('modal', { static: true }) modal: ElementRef + + videoChangeOwnership: VideoChangeOwnership | undefined = undefined + + videoChannels: VideoChannel[] + + error: string = null + + constructor ( + protected formValidatorService: FormValidatorService, + private videoOwnershipService: VideoOwnershipService, + private notifier: Notifier, + private authService: AuthService, + private videoChannelService: VideoChannelService, + private modalService: NgbModal + ) { + super() + } + + ngOnInit () { + this.videoChannels = [] + + this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account) + .subscribe(videoChannels => this.videoChannels = videoChannels.data) + + this.buildForm({ + channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR + }) + } + + show (videoChangeOwnership: VideoChangeOwnership) { + this.videoChangeOwnership = videoChangeOwnership + this.modalService + .open(this.modal, { centered: true }) + .result + .then(() => this.acceptOwnership()) + .catch(() => this.videoChangeOwnership = undefined) + } + + acceptOwnership () { + const channel = this.form.value['channel'] + + const videoChangeOwnership = this.videoChangeOwnership + this.videoOwnershipService + .acceptOwnership(videoChangeOwnership.id, { channelId: channel }) + .subscribe( + () => { + this.notifier.success($localize`Ownership accepted`) + if (this.accepted) this.accepted.emit() + this.videoChangeOwnership = undefined + }, + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.html b/client/src/app/+my-library/my-ownership/my-ownership.component.html new file mode 100644 index 000000000..6bf562986 --- /dev/null +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.html @@ -0,0 +1,90 @@ +

+ + My ownership changes +

+ + + + + Actions + Initiator + Video + + Created + + + Status + + + + + + + + + + + + + +
+ Avatar +
+ {{ videoChangeOwnership.initiatorAccount.displayName }} + {{ videoChangeOwnership.initiatorAccount.nameWithHost }} +
+
+
+ + + + +
+
+ +
+
+
+ {{ videoChangeOwnership.video.name }} +
+
by {{ videoChangeOwnership.video.channel?.displayName }}
+
+
+
+ + + {{ videoChangeOwnership.createdAt | date: 'short' }} + + + {{ videoChangeOwnership.status }} + + +
+ + + + +
+ No ownership change request found. +
+ + +
+
+ + diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-ownership.component.scss new file mode 100644 index 000000000..7cac9c9f3 --- /dev/null +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.scss @@ -0,0 +1,71 @@ +@import 'miniature'; +@import 'mixins'; + +.chip { + @include chip; +} + +.badge { + @include table-badge; +} + +.video-table-video { + display: inline-flex; + + .video-table-video-image { + @include miniature-thumbnail; + + $image-height: 45px; + + height: $image-height; + width: #{(16/9) * $image-height}; + margin-right: 0.5rem; + border-radius: 2px; + border: none; + background: transparent; + display: inline-flex; + justify-content: center; + align-items: center; + position: relative; + + img { + height: 100%; + width: 100%; + border-radius: 2px; + } + + span { + color: pvar(--inputPlaceholderColor); + } + + .video-table-video-image-label { + @include static-thumbnail-overlay; + position: absolute; + border-radius: 3px; + font-size: 10px; + padding: 0 3px; + line-height: 1.3; + bottom: 2px; + right: 2px; + } + } + + .video-table-video-text { + display: inline-flex; + flex-direction: column; + justify-content: center; + font-size: 90%; + color: pvar(--mainForegroundColor); + line-height: 1rem; + + div .glyphicon { + font-size: 80%; + color: gray; + margin-left: 0.1rem; + } + + div + div { + font-size: 80%; + } + } +} diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-ownership.component.ts new file mode 100644 index 000000000..e1aca65f6 --- /dev/null +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.ts @@ -0,0 +1,81 @@ +import { SortMeta } from 'primeng/api' +import { Component, OnInit, ViewChild } from '@angular/core' +import { Notifier, RestPagination, RestTable } from '@app/core' +import { Account, Actor, VideoOwnershipService } from '@app/shared/shared-main' +import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models' +import { MyAcceptOwnershipComponent } from './my-accept-ownership/my-accept-ownership.component' + +@Component({ + templateUrl: './my-ownership.component.html', + styleUrls: [ './my-ownership.component.scss' ] +}) +export class MyOwnershipComponent extends RestTable implements OnInit { + videoChangeOwnerships: VideoChangeOwnership[] = [] + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: -1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + @ViewChild('myAcceptOwnershipComponent', { static: true }) myAccountAcceptOwnershipComponent: MyAcceptOwnershipComponent + + constructor ( + private notifier: Notifier, + private videoOwnershipService: VideoOwnershipService + ) { + super() + } + + ngOnInit () { + this.initialize() + } + + getIdentifier () { + return 'MyOwnershipComponent' + } + + getStatusClass (status: VideoChangeOwnershipStatus) { + switch (status) { + case VideoChangeOwnershipStatus.ACCEPTED: + return 'badge-green' + case VideoChangeOwnershipStatus.REFUSED: + return 'badge-red' + default: + return 'badge-yellow' + } + } + + switchToDefaultAvatar ($event: Event) { + ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() + } + + openAcceptModal (videoChangeOwnership: VideoChangeOwnership) { + this.myAccountAcceptOwnershipComponent.show(videoChangeOwnership) + } + + accepted () { + this.loadData() + } + + refuse (videoChangeOwnership: VideoChangeOwnership) { + this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) + .subscribe( + () => this.loadData(), + err => this.notifier.error(err.message) + ) + } + + protected loadData () { + return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) + .subscribe( + resultList => { + this.videoChangeOwnerships = resultList.data.map(change => ({ + ...change, + initiatorAccount: new Account(change.initiatorAccount), + nextOwnerAccount: new Account(change.nextOwnerAccount) + })) + this.totalRecords = resultList.total + }, + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html new file mode 100644 index 000000000..6ab3826ba --- /dev/null +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html @@ -0,0 +1,42 @@ +

+ + + My subscriptions + {{ pagination.totalItems }} + +

+ +
+
+ + + Clear filters +
+
+ +
You don't have any subscriptions yet.
+ + diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss new file mode 100644 index 000000000..5ead45dd8 --- /dev/null +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss @@ -0,0 +1,81 @@ +@import '_variables'; +@import '_mixins'; + +input[type=text] { + @include peertube-input-text(300px); +} + +.video-channel { + @include row-blocks; + + img { + @include avatar(80px); + + margin-right: 10px; + } + + .video-channel-info { + flex-grow: 1; + + a.video-channel-names { + @include disable-default-a-behaviour; + + width: fit-content; + display: flex; + align-items: baseline; + color: pvar(--mainForegroundColor); + + .video-channel-display-name { + font-weight: $font-semibold; + font-size: 18px; + } + + .video-channel-name { + font-size: 14px; + color: $grey-actor-name; + margin-left: 5px; + } + } + } + + .actor-owner { + @include actor-owner; + + margin-top: 0; + } +} + +.video-subscriptions-header { + margin-bottom: 30px; +} + +@media screen and (max-width: $small-view) { + .video-channel { + .video-channel-info { + padding-bottom: 10px; + text-align: center; + + .video-channel-names { + flex-direction: column; + align-items: center !important; + margin: auto; + } + } + + img { + margin-right: 0; + } + } +} + +@media screen and (max-width: $mobile-view) { + .video-subscriptions-header { + flex-direction: column; + + input[type=text] { + width: 100% !important; + } + } +} + + diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts new file mode 100644 index 000000000..3b748eccf --- /dev/null +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts @@ -0,0 +1,74 @@ +import { Subject } from 'rxjs' +import { debounceTime } from 'rxjs/operators' +import { Component, OnInit } from '@angular/core' +import { ComponentPagination, Notifier } from '@app/core' +import { VideoChannel } from '@app/shared/shared-main' +import { UserSubscriptionService } from '@app/shared/shared-user-subscription' + +@Component({ + templateUrl: './my-subscriptions.component.html', + styleUrls: [ './my-subscriptions.component.scss' ] +}) +export class MySubscriptionsComponent implements OnInit { + videoChannels: VideoChannel[] = [] + + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: null + } + + onDataSubject = new Subject() + + subscriptionsSearch: string + subscriptionsSearchChanged = new Subject() + + constructor ( + private userSubscriptionService: UserSubscriptionService, + private notifier: Notifier + ) {} + + ngOnInit () { + this.loadSubscriptions() + + this.subscriptionsSearchChanged + .pipe(debounceTime(500)) + .subscribe(() => { + this.pagination.currentPage = 1 + this.loadSubscriptions(false) + }) + } + + resetSearch () { + this.subscriptionsSearch = '' + this.onSubscriptionsSearchChanged() + } + + onSubscriptionsSearchChanged () { + this.subscriptionsSearchChanged.next() + } + + onNearOfBottom () { + // Last page + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + + this.pagination.currentPage += 1 + this.loadSubscriptions() + } + + private loadSubscriptions (more = true) { + this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch }) + .subscribe( + res => { + this.videoChannels = more + ? this.videoChannels.concat(res.data) + : res.data + this.pagination.totalItems = res.total + + this.onDataSubject.next(res.data) + }, + + error => this.notifier.error(error.message) + ) + } +} diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html new file mode 100644 index 000000000..1d3a45f76 --- /dev/null +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html @@ -0,0 +1,70 @@ +

+ + My imports +

+ + + + + + Action + Target + Video + State + Created + + + + + + + + + + + + + + + + + {{ videoImport.targetUrl }} + + {{ videoImport.torrentName || videoImport.magnetUri }} + + + + + {{ videoImport.video?.name }} + + {{ videoImport.video?.name }} + + This video was deleted + + + + + + {{ videoImport.state.label }} + + + + {{ videoImport.createdAt | date: 'short' }} + + + + + + +
{{ videoImport.error }}
+ + +
+
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss b/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss new file mode 100644 index 000000000..a93c28028 --- /dev/null +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss @@ -0,0 +1,14 @@ +@import '_variables'; +@import '_mixins'; + +pre { + font-size: 11px; +} + +.video-import-error { + color: red; +} + +.badge { + @include table-badge; +} diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts new file mode 100644 index 000000000..d6d7d7a1b --- /dev/null +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts @@ -0,0 +1,76 @@ +import { SortMeta } from 'primeng/api' +import { Component, OnInit } from '@angular/core' +import { Notifier, RestPagination, RestTable } from '@app/core' +import { VideoImportService } from '@app/shared/shared-main' +import { VideoImport, VideoImportState } from '@shared/models' + +@Component({ + templateUrl: './my-video-imports.component.html', + styleUrls: [ './my-video-imports.component.scss' ] +}) +export class MyVideoImportsComponent extends RestTable implements OnInit { + videoImports: VideoImport[] = [] + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: 1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + constructor ( + private notifier: Notifier, + private videoImportService: VideoImportService + ) { + super() + } + + ngOnInit () { + this.initialize() + } + + getIdentifier () { + return 'MyVideoImportsComponent' + } + + getVideoImportStateClass (state: VideoImportState) { + switch (state) { + case VideoImportState.FAILED: + return 'badge-red' + case VideoImportState.REJECTED: + return 'badge-banned' + case VideoImportState.PENDING: + return 'badge-yellow' + default: + return 'badge-green' + } + } + + isVideoImportSuccess (videoImport: VideoImport) { + return videoImport.state.id === VideoImportState.SUCCESS + } + + isVideoImportPending (videoImport: VideoImport) { + return videoImport.state.id === VideoImportState.PENDING + } + + isVideoImportFailed (videoImport: VideoImport) { + return videoImport.state.id === VideoImportState.FAILED + } + + getVideoUrl (video: { uuid: string }) { + return '/videos/watch/' + video.uuid + } + + getEditVideoUrl (video: { uuid: string }) { + return '/videos/update/' + video.uuid + } + + protected loadData () { + this.videoImportService.getMyVideoImports(this.pagination, this.sort) + .subscribe( + resultList => { + this.videoImports = resultList.data + this.totalRecords = resultList.total + }, + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts new file mode 100644 index 000000000..5abea54b0 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts @@ -0,0 +1,91 @@ +import { Component, OnInit } from '@angular/core' +import { Router } from '@angular/router' +import { AuthService, Notifier, ServerService } from '@app/core' +import { populateAsyncUserVideoChannels } from '@app/helpers' +import { + setPlaylistChannelValidator, + VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, + VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, + VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, + VIDEO_PLAYLIST_PRIVACY_VALIDATOR +} from '@app/shared/form-validators/video-playlist-validators' +import { FormValidatorService } from '@app/shared/shared-forms' +import { VideoPlaylistService } from '@app/shared/shared-video-playlist' +import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' +import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' +import { MyVideoPlaylistEdit } from './my-video-playlist-edit' + +@Component({ + templateUrl: './my-video-playlist-edit.component.html', + styleUrls: [ './my-video-playlist-edit.component.scss' ] +}) +export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implements OnInit { + error: string + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private notifier: Notifier, + private router: Router, + private videoPlaylistService: VideoPlaylistService, + private serverService: ServerService + ) { + super() + } + + ngOnInit () { + this.buildForm({ + displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, + privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR, + description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, + videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, + thumbnailfile: null + }) + + this.form.get('privacy').valueChanges.subscribe(privacy => { + setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) + }) + + populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) + .catch(err => console.error('Cannot populate user video channels.', err)) + + this.serverService.getVideoPlaylistPrivacies() + .subscribe(videoPlaylistPrivacies => { + this.videoPlaylistPrivacies = videoPlaylistPrivacies + + this.form.patchValue({ + privacy: VideoPlaylistPrivacy.PRIVATE + }) + }) + } + + formValidated () { + this.error = undefined + + const body = this.form.value + const videoPlaylistCreate: VideoPlaylistCreate = { + displayName: body.displayName, + privacy: body.privacy, + description: body.description || null, + videoChannelId: body.videoChannelId || null, + thumbnailfile: body.thumbnailfile || null + } + + this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe( + () => { + this.notifier.success($localize`Playlist ${videoPlaylistCreate.displayName} created.`) + this.router.navigate([ '/my-library', 'video-playlists' ]) + }, + + err => this.error = err.message + ) + } + + isCreation () { + return true + } + + getFormButtonTitle () { + return $localize`Create` + } +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html new file mode 100644 index 000000000..0d8d2a447 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html @@ -0,0 +1,100 @@ + + +
{{ error }}
+ +
+ +
+
+
NEW PLAYLIST
+
PLAYLIST
+
+ +
+ +
+
+ + +
+ {{ formErrors['displayName'] }} +
+
+ +
+ + +
+ {{ formErrors.description }} +
+
+
+ +
+
+ +
+ +
+ +
+ {{ formErrors.privacy }} +
+
+ +
+ + + + +
+ {{ formErrors['videoChannelId'] }} +
+
+ +
+ + + +
+
+ +
+
+
+ +
+
+
+
+ +
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss new file mode 100644 index 000000000..08fab1101 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss @@ -0,0 +1,36 @@ +@import '_variables'; +@import '_mixins'; + +label { + font-weight: $font-regular; + font-size: 100%; +} + +.video-playlist-title { + @include settings-big-title; +} + +input[type=text] { + @include peertube-input-text(340px); + + display: block; +} + +textarea { + @include peertube-textarea(500px, 150px); + + display: block; +} + +.peertube-select-container { + @include peertube-select-container(340px); +} + +input[type=submit] { + @include peertube-button; + @include orange-button; +} + +.breadcrumb { + @include breadcrumb; +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts new file mode 100644 index 000000000..40ba23e75 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.ts @@ -0,0 +1,13 @@ +import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms' +import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' +import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' + +export abstract class MyVideoPlaylistEdit extends FormReactive { + // Declare it here to avoid errors in create template + videoPlaylistToUpdate: VideoPlaylist + userVideoChannels: SelectChannelItem[] = [] + videoPlaylistPrivacies: VideoConstant[] = [] + + abstract isCreation (): boolean + abstract getFormButtonTitle (): string +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html new file mode 100644 index 000000000..09b4c8a1b --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html @@ -0,0 +1,51 @@ +
+ +
+ + +
+ + + +
+ +
+ +
+
+
No videos in this playlist.
+ +
+ Browse videos on PeerTube to add them in your playlist. +
+ +
+ See the documentation for more information. +
+
+ +
+
+ + +
+
+
+
+ + diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss new file mode 100644 index 000000000..de7e1993f --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss @@ -0,0 +1,83 @@ +@import '_variables'; +@import '_mixins'; +@import '_miniature'; + +.playlist-info { + background-color: pvar(--submenuColor); + margin-left: -$not-expanded-horizontal-margins; + margin-top: -$sub-menu-margin-bottom; + + padding: 10px; + + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + + /* fix ellipsis dots background color */ + ::ng-deep .miniature-name::after { + background-color: pvar(--submenuColor) !important; + } +} + +.playlist-buttons { + display:flex; + margin: 30px 0 10px 0; + + .share-button { + @include peertube-button; + @include button-with-icon(17px, 3px, -1px); + @include grey-button; + @include apply-svg-color(pvar(--actionButtonColor)); + + margin-right: 10px; + } +} + +// Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.video:last-child { + border: none; +} + +.videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +@media screen and (max-width: $small-view) { + .playlist-info { + width: 100vw; + padding-top: 20px; + margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); + } + + .playlist-elements { + padding: 0 !important; + } + + ::ng-deep my-video-playlist-element-miniature { + + .video { + padding: 5px !important; + } + + .position { + margin-right: 5px !important; + } + } +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts new file mode 100644 index 000000000..a8fdf6e29 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts @@ -0,0 +1,197 @@ +import { Subject, Subscription } from 'rxjs' +import { CdkDragDrop } from '@angular/cdk/drag-drop' +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { ComponentPagination, ConfirmService, Notifier, ScreenService } from '@app/core' +import { DropdownAction } from '@app/shared/shared-main' +import { VideoShareComponent } from '@app/shared/shared-share-modal' +import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' +import { VideoPlaylistType } from '@shared/models' + +@Component({ + templateUrl: './my-video-playlist-elements.component.html', + styleUrls: [ './my-video-playlist-elements.component.scss' ] +}) +export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { + @ViewChild('videoShareModal') videoShareModal: VideoShareComponent + + playlistElements: VideoPlaylistElement[] = [] + playlist: VideoPlaylist + + playlistActions: DropdownAction[][] = [] + + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: null + } + + onDataSubject = new Subject() + + private videoPlaylistId: string | number + private paramsSub: Subscription + + constructor ( + private notifier: Notifier, + private router: Router, + private confirmService: ConfirmService, + private route: ActivatedRoute, + private screenService: ScreenService, + private videoPlaylistService: VideoPlaylistService + ) {} + + ngOnInit () { + this.playlistActions = [ + [ + { + label: $localize`Update playlist`, + iconName: 'edit', + linkBuilder: playlist => [ '/my-library', 'video-playlists', 'update', playlist.uuid ] + }, + { + label: $localize`Delete playlist`, + iconName: 'delete', + handler: playlist => this.deleteVideoPlaylist(playlist) + } + ] + ] + + this.paramsSub = this.route.params.subscribe(routeParams => { + this.videoPlaylistId = routeParams[ 'videoPlaylistId' ] + this.loadElements() + + this.loadPlaylistInfo() + }) + } + + ngOnDestroy () { + if (this.paramsSub) this.paramsSub.unsubscribe() + } + + drop (event: CdkDragDrop) { + const previousIndex = event.previousIndex + const newIndex = event.currentIndex + + if (previousIndex === newIndex) return + + const oldPosition = this.playlistElements[previousIndex].position + let insertAfter = this.playlistElements[newIndex].position + + if (oldPosition > insertAfter) insertAfter-- + + const element = this.playlistElements[previousIndex] + + this.playlistElements.splice(previousIndex, 1) + this.playlistElements.splice(newIndex, 0, element) + + this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) + .subscribe( + () => { + this.reorderClientPositions() + }, + + err => this.notifier.error(err.message) + ) + } + + onElementRemoved (element: VideoPlaylistElement) { + const oldFirst = this.findFirst() + + this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) + this.reorderClientPositions(oldFirst) + } + + onNearOfBottom () { + // Last page + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + + this.pagination.currentPage += 1 + this.loadElements() + } + + trackByFn (index: number, elem: VideoPlaylistElement) { + return elem.id + } + + isRegularPlaylist (playlist: VideoPlaylist) { + return playlist?.type.id === VideoPlaylistType.REGULAR + } + + showShareModal () { + this.videoShareModal.show() + } + + async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) { + const res = await this.confirmService.confirm( + $localize`Do you really want to delete ${videoPlaylist.displayName}?`, + $localize`Delete` + ) + if (res === false) return + + this.videoPlaylistService.removeVideoPlaylist(videoPlaylist) + .subscribe( + () => { + this.router.navigate([ '/my-library', 'video-playlists' ]) + this.notifier.success($localize`Playlist ${videoPlaylist.displayName} deleted.`) + }, + + error => this.notifier.error(error.message) + ) + } + + /** + * Returns null to not have drag and drop delay. + * In small views, where elements are about 100% wide, + * we add a delay to prevent unwanted drag&drop. + * + * @see {@link https://github.com/Chocobozzz/PeerTube/issues/2078} + * + * @returns {null|number} Null for no delay, or a number in milliseconds. + */ + getDragStartDelay (): null | number { + if (this.screenService.isInTouchScreen()) { + return 500 + } + + return null + } + + private loadElements () { + this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination) + .subscribe(({ total, data }) => { + this.playlistElements = this.playlistElements.concat(data) + this.pagination.totalItems = total + + this.onDataSubject.next(data) + }) + } + + private loadPlaylistInfo () { + this.videoPlaylistService.getVideoPlaylist(this.videoPlaylistId) + .subscribe(playlist => { + this.playlist = playlist + }) + } + + private reorderClientPositions (first?: VideoPlaylistElement) { + if (this.playlistElements.length === 0) return + + const oldFirst = first || this.findFirst() + let i = 1 + + for (const element of this.playlistElements) { + element.position = i + i++ + } + + // Reload playlist thumbnail if the first element changed + const newFirst = this.findFirst() + if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { + this.playlist.refreshThumbnail() + } + } + + private findFirst () { + return this.playlistElements.find(e => e.position === 1) + } +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts new file mode 100644 index 000000000..532423ba2 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts @@ -0,0 +1,129 @@ +import { forkJoin, Subscription } from 'rxjs' +import { map, switchMap } from 'rxjs/operators' +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { AuthService, Notifier, ServerService } from '@app/core' +import { populateAsyncUserVideoChannels } from '@app/helpers' +import { + setPlaylistChannelValidator, + VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, + VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, + VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, + VIDEO_PLAYLIST_PRIVACY_VALIDATOR +} from '@app/shared/form-validators/video-playlist-validators' +import { FormValidatorService } from '@app/shared/shared-forms' +import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' +import { VideoPlaylistUpdate } from '@shared/models' +import { MyVideoPlaylistEdit } from './my-video-playlist-edit' + +@Component({ + templateUrl: './my-video-playlist-edit.component.html', + styleUrls: [ './my-video-playlist-edit.component.scss' ] +}) +export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implements OnInit, OnDestroy { + error: string + videoPlaylistToUpdate: VideoPlaylist + + private paramsSub: Subscription + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private notifier: Notifier, + private router: Router, + private route: ActivatedRoute, + private videoPlaylistService: VideoPlaylistService, + private serverService: ServerService + ) { + super() + } + + ngOnInit () { + this.buildForm({ + displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, + privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR, + description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, + videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, + thumbnailfile: null + }) + + this.form.get('privacy').valueChanges.subscribe(privacy => { + setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) + }) + + populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) + .catch(err => console.error('Cannot populate user video channels.', err)) + + this.paramsSub = this.route.params + .pipe( + map(routeParams => routeParams['videoPlaylistId']), + switchMap(videoPlaylistId => { + return forkJoin([ + this.videoPlaylistService.getVideoPlaylist(videoPlaylistId), + this.serverService.getVideoPlaylistPrivacies() + ]) + }) + ) + .subscribe( + ([ videoPlaylistToUpdate, videoPlaylistPrivacies]) => { + this.videoPlaylistToUpdate = videoPlaylistToUpdate + this.videoPlaylistPrivacies = videoPlaylistPrivacies + + this.hydrateFormFromPlaylist() + }, + + err => this.error = err.message + ) + } + + ngOnDestroy () { + if (this.paramsSub) this.paramsSub.unsubscribe() + } + + formValidated () { + this.error = undefined + + const body = this.form.value + const videoPlaylistUpdate: VideoPlaylistUpdate = { + displayName: body.displayName, + privacy: body.privacy, + description: body.description || null, + videoChannelId: body.videoChannelId || null, + thumbnailfile: body.thumbnailfile || undefined + } + + this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe( + () => { + this.notifier.success($localize`Playlist ${videoPlaylistUpdate.displayName} updated.`) + this.router.navigate([ '/my-library', 'video-playlists' ]) + }, + + err => this.error = err.message + ) + } + + isCreation () { + return false + } + + getFormButtonTitle () { + return $localize`Update` + } + + private hydrateFormFromPlaylist () { + this.form.patchValue({ + displayName: this.videoPlaylistToUpdate.displayName, + privacy: this.videoPlaylistToUpdate.privacy.id, + description: this.videoPlaylistToUpdate.description, + videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null + }) + + fetch(this.videoPlaylistToUpdate.thumbnailUrl) + .then(response => response.blob()) + .then(data => { + this.form.patchValue({ + thumbnailfile: data + }) + }) + } +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html new file mode 100644 index 000000000..afcf6a084 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html @@ -0,0 +1,35 @@ +

+ + + My playlists {{ pagination.totalItems }} + +

+ +
+
+ + + Clear filters +
+ + + + Create playlist + +
+ +
+
+
+ +
+ +
+ + + +
+
+
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss new file mode 100644 index 000000000..2b7c88246 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss @@ -0,0 +1,78 @@ +@import '_variables'; +@import '_mixins'; + +.create-button { + @include create-button; +} + +input[type=text] { + @include peertube-input-text(300px); +} + +::ng-deep .action-button { + &.action-button-delete { + margin-right: 10px; + } +} + +.video-playlist { + @include row-blocks; + + .miniature-wrapper { + flex-grow: 1; + + ::ng-deep .miniature { + display: flex; + + .miniature-info { + margin-left: 10px; + width: auto; + } + } + } + + .video-playlist-buttons { + min-width: 190px; + height: max-content; + } +} + +.video-playlists-header { + margin-bottom: 30px; +} + +@media screen and (max-width: $small-view) { + .video-playlists-header { + text-align: center; + } + + .video-playlist { + + .video-playlist-buttons { + margin-top: 10px; + } + } + + my-video-playlist-miniature ::ng-deep .miniature { + flex-direction: column; + + .miniature-info { + margin-left: 0 !important; + } + + .miniature-name { + max-width: $video-thumbnail-width; + } + } +} + +@media screen and (max-width: $mobile-view) { + .video-playlists-header { + flex-direction: column; + + input[type=text] { + width: 100% !important; + margin-bottom: 12px; + } + } +} diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts new file mode 100644 index 000000000..f6d394923 --- /dev/null +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts @@ -0,0 +1,101 @@ +import { Subject } from 'rxjs' +import { debounceTime, mergeMap } from 'rxjs/operators' +import { Component, OnInit } from '@angular/core' +import { AuthService, ComponentPagination, ConfirmService, Notifier, User } from '@app/core' +import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' +import { VideoPlaylistType } from '@shared/models' + +@Component({ + templateUrl: './my-video-playlists.component.html', + styleUrls: [ './my-video-playlists.component.scss' ] +}) +export class MyVideoPlaylistsComponent implements OnInit { + videoPlaylistsSearch: string + videoPlaylists: VideoPlaylist[] = [] + videoPlaylistSearchChanged = new Subject() + + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 5, + totalItems: null + } + + onDataSubject = new Subject() + + private user: User + + constructor ( + private authService: AuthService, + private notifier: Notifier, + private confirmService: ConfirmService, + private videoPlaylistService: VideoPlaylistService + ) {} + + ngOnInit () { + this.user = this.authService.getUser() + + this.loadVideoPlaylists() + + this.videoPlaylistSearchChanged + .pipe( + debounceTime(500)) + .subscribe(() => { + this.loadVideoPlaylists(true) + }) + } + + async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) { + const res = await this.confirmService.confirm( + $localize`Do you really want to delete ${videoPlaylist.displayName}?`, + $localize`Delete` + ) + if (res === false) return + + this.videoPlaylistService.removeVideoPlaylist(videoPlaylist) + .subscribe( + () => { + this.videoPlaylists = this.videoPlaylists + .filter(p => p.id !== videoPlaylist.id) + + this.notifier.success($localize`Playlist ${videoPlaylist.displayName}} deleted.`) + }, + + error => this.notifier.error(error.message) + ) + } + + isRegularPlaylist (playlist: VideoPlaylist) { + return playlist.type.id === VideoPlaylistType.REGULAR + } + + onNearOfBottom () { + // Last page + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + + this.pagination.currentPage += 1 + this.loadVideoPlaylists() + } + + resetSearch () { + this.videoPlaylistsSearch = '' + this.onVideoPlaylistSearchChanged() + } + + onVideoPlaylistSearchChanged () { + this.videoPlaylistSearchChanged.next() + } + + private loadVideoPlaylists (reset = false) { + this.authService.userInformationLoaded + .pipe(mergeMap(() => { + return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt', this.videoPlaylistsSearch) + })) + .subscribe(res => { + if (reset) this.videoPlaylists = [] + this.videoPlaylists = this.videoPlaylists.concat(res.data) + this.pagination.totalItems = res.total + + this.onDataSubject.next(res.data) + }) + } +} diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html new file mode 100644 index 000000000..c7c5a0b69 --- /dev/null +++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html @@ -0,0 +1,33 @@ + + + + + + + diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss new file mode 100644 index 000000000..a79fec179 --- /dev/null +++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss @@ -0,0 +1,10 @@ +@import '_variables'; +@import '_mixins'; + +p-autocomplete { + display: block; +} + +.form-group { + margin: 20px 0; +} \ No newline at end of file diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts new file mode 100644 index 000000000..84237dee1 --- /dev/null +++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts @@ -0,0 +1,69 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' +import { Notifier, UserService } from '@app/core' +import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' +import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { Video, VideoOwnershipService } from '@app/shared/shared-main' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' + +@Component({ + selector: 'my-video-change-ownership', + templateUrl: './video-change-ownership.component.html', + styleUrls: [ './video-change-ownership.component.scss' ] +}) +export class VideoChangeOwnershipComponent extends FormReactive implements OnInit { + @ViewChild('modal', { static: true }) modal: ElementRef + + usernamePropositions: string[] + + error: string = null + + private video: Video | undefined = undefined + + constructor ( + protected formValidatorService: FormValidatorService, + private videoOwnershipService: VideoOwnershipService, + private notifier: Notifier, + private userService: UserService, + private modalService: NgbModal + ) { + super() + } + + ngOnInit () { + this.buildForm({ + username: OWNERSHIP_CHANGE_USERNAME_VALIDATOR + }) + this.usernamePropositions = [] + } + + show (video: Video) { + this.video = video + this.modalService + .open(this.modal, { centered: true }) + .result + .then(() => this.changeOwnership()) + .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing + } + + search (event: { query: string }) { + const query = event.query + this.userService.autocomplete(query) + .subscribe( + usernames => this.usernamePropositions = usernames, + + err => this.notifier.error(err.message) + ) + } + + changeOwnership () { + const username = this.form.value['username'] + + this.videoOwnershipService + .changeOwnership(this.video.id, username) + .subscribe( + () => this.notifier.success($localize`Ownership change request sent.`), + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html new file mode 100644 index 000000000..977f7b03b --- /dev/null +++ b/client/src/app/+my-library/my-videos/my-videos.component.html @@ -0,0 +1,58 @@ +

+ + + My videos + {{ pagination.totalItems }} + + + +

+ +
+
+ + + Clear filters +
+
+ + + + + + Delete + + + + +
+ + + +
+
+
+ + + + diff --git a/client/src/app/+my-library/my-videos/my-videos.component.scss b/client/src/app/+my-library/my-videos/my-videos.component.scss new file mode 100644 index 000000000..59fc5fe80 --- /dev/null +++ b/client/src/app/+my-library/my-videos/my-videos.component.scss @@ -0,0 +1,138 @@ +@import '_variables'; +@import '_mixins'; + +input[type=text] { + @include peertube-input-text(300px); +} + +h1 { + display: flex; + justify-content: space-between; + + .button-link { + @include peertube-button-link; + @include grey-button; + @include button-with-icon(18px, 3px, -1px); + + &:not(:last-child) { + margin-right: 10px; + } + } +} + +.action-button-delete-selection { + display: inline-block; + + @include peertube-button; + @include orange-button; + @include button-with-icon(21px); + + my-global-icon { + @include apply-svg-color(#fff); + } +} + +::ng-deep { + .video { + flex-wrap: wrap; + } + + .action-button span { + white-space: nowrap; + } + + .video-miniature { + &.display-as-row { + // width: min-content !important; + width: 100% !important; + + .video-bottom .video-miniature-information { + width: max-content !important; + min-width: unset !important; + } + } + + .video-bottom { + max-width: 350px; + } + } +} + +.action-button { + display: flex; + margin-left: 55px; + margin-top: 10px; + align-self: flex-end; +} + +my-edit-button { + margin-right: 10px; +} + +@media screen and (max-width: $small-view) { + h1 { + flex-direction: column; + + > span, + .button-link { + margin-bottom: 10px; + } + } + + .action-button { + flex-direction: column; + align-self: center; + align-items: center; + margin-left: 0px; + } + + my-edit-button { + margin: 15px 0 5px 0; + width: 100%; + text-align: center; + + ::ng-deep { + .action-button { + /* same width than a.video-thumbnail */ + width: $video-thumbnail-width; + } + } + } + + ::ng-deep { + .video-miniature { + align-items: center; + + .video-bottom, + .video-bottom .video-miniature-information { + /* same width than a.video-thumbnail */ + max-width: $video-thumbnail-width !important; + } + } + } +} + +// Adapt my-video-miniature on small screens with menu +@media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) { + :host-context(.main-col:not(.expanded)) { + ::ng-deep { + .video-miniature { + flex-direction: column; + + .video-miniature-name { + max-width: $video-thumbnail-width; + } + } + } + } +} + +@media screen and (max-width: $mobile-view) { + .videos-header { + flex-direction: column; + + input[type=text] { + width: 100% !important; + } + } +} diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts new file mode 100644 index 000000000..e89bb12e1 --- /dev/null +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts @@ -0,0 +1,178 @@ +import { concat, Observable, Subject } from 'rxjs' +import { debounceTime, tap, toArray } from 'rxjs/operators' +import { Component, OnInit, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' +import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' +import { immutableAssign } from '@app/helpers' +import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' +import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' +import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' +import { VideoSortField } from '@shared/models' +import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' + +@Component({ + templateUrl: './my-videos.component.html', + styleUrls: [ './my-videos.component.scss' ] +}) +export class MyVideosComponent implements OnInit, DisableForReuseHook { + @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent + @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent + @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent + + titlePage: string + selection: SelectionType = {} + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: null + } + miniatureDisplayOptions: MiniatureDisplayOptions = { + date: true, + views: true, + by: true, + privacyLabel: false, + privacyText: true, + state: true, + blacklistInfo: true + } + ownerDisplayType: OwnerDisplayType = 'videoChannel' + + videoActions: DropdownAction<{ video: Video }>[] = [] + + videos: Video[] = [] + videosSearch: string + videosSearchChanged = new Subject() + getVideosObservableFunction = this.getVideosObservable.bind(this) + + constructor ( + protected router: Router, + protected serverService: ServerService, + protected route: ActivatedRoute, + protected authService: AuthService, + protected notifier: Notifier, + protected screenService: ScreenService, + private confirmService: ConfirmService, + private videoService: VideoService + ) { + this.titlePage = $localize`My videos` + } + + ngOnInit () { + this.buildActions() + + this.videosSearchChanged + .pipe(debounceTime(500)) + .subscribe(() => { + this.videosSelection.reloadVideos() + }) + } + + resetSearch () { + this.videosSearch = '' + this.onVideosSearchChanged() + } + + onVideosSearchChanged () { + this.videosSearchChanged.next() + } + + disableForReuse () { + this.videosSelection.disableForReuse() + } + + enabledForReuse () { + this.videosSelection.enabledForReuse() + } + + getVideosObservable (page: number, sort: VideoSortField) { + const newPagination = immutableAssign(this.pagination, { currentPage: page }) + + return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) + .pipe( + tap(res => this.pagination.totalItems = res.total) + ) + } + + async deleteSelectedVideos () { + const toDeleteVideosIds = Object.keys(this.selection) + .filter(k => this.selection[ k ] === true) + .map(k => parseInt(k, 10)) + + const res = await this.confirmService.confirm( + $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`, + $localize`Delete` + ) + if (res === false) return + + const observables: Observable[] = [] + for (const videoId of toDeleteVideosIds) { + const o = this.videoService.removeVideo(videoId) + .pipe(tap(() => this.removeVideoFromArray(videoId))) + + observables.push(o) + } + + concat(...observables) + .pipe(toArray()) + .subscribe( + () => { + this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`) + this.selection = {} + }, + + err => this.notifier.error(err.message) + ) + } + + async deleteVideo (video: Video) { + const res = await this.confirmService.confirm( + $localize`Do you really want to delete ${video.name}?`, + $localize`Delete` + ) + if (res === false) return + + this.videoService.removeVideo(video.id) + .subscribe( + () => { + this.notifier.success($localize`Video ${video.name} deleted.`) + this.removeVideoFromArray(video.id) + }, + + error => this.notifier.error(error.message) + ) + } + + changeOwnership (video: Video) { + this.videoChangeOwnershipModal.show(video) + } + + displayLiveInformation (video: Video) { + this.liveStreamInformationModal.show(video) + } + + private removeVideoFromArray (id: number) { + this.videos = this.videos.filter(v => v.id !== id) + } + + private buildActions () { + this.videoActions = [ + { + label: $localize`Display live information`, + handler: ({ video }) => this.displayLiveInformation(video), + isDisplayed: ({ video }) => video.isLive, + iconName: 'live' + }, + { + label: $localize`Change ownership`, + handler: ({ video }) => this.changeOwnership(video), + iconName: 'ownership-change' + }, + { + label: $localize`Delete`, + handler: ({ video }) => this.deleteVideo(video), + iconName: 'delete' + } + ] + } +} diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 43b5cd92e..4b0d12b6e 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html @@ -18,7 +18,7 @@
- + Manage channel diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts index 64e887987..caa7af90a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts @@ -128,7 +128,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca this.isUpdatingVideo = false this.notifier.success($localize`Video to import updated.`) - this.router.navigate([ '/my-account', 'video-imports' ]) + this.router.navigate([ '/my-library', 'video-imports' ]) }, err => { diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts index 47f59a5d0..5ca753eaf 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts @@ -138,7 +138,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom this.isUpdatingVideo = false this.notifier.success($localize`Video to import updated.`) - this.router.navigate([ '/my-account', 'video-imports' ]) + this.router.navigate([ '/my-library', 'video-imports' ]) }, err => { diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts index b02988169..6988c574b 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts @@ -34,7 +34,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement this.titlePage = $localize`Videos from your subscriptions` this.actions.push({ - routerLink: '/my-account/subscriptions', + routerLink: '/my-library/subscriptions', label: $localize`Subscriptions`, iconName: 'cog' }) diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index d3cdcefea..bcae29c9a 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -17,6 +17,10 @@ const routes: Routes = [ path: 'my-account', loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule) }, + { + path: 'my-library', + loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule) + }, { path: 'verify-account', loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule) diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 10e2c2c58..3410c5a6f 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -62,15 +62,15 @@ export class AuthService { return false }, undefined, $localize`Go to my subscriptions`), new Hotkey('m v', (event: KeyboardEvent): boolean => { - this.router.navigate([ '/my-account/videos' ]) + this.router.navigate([ '/my-library/videos' ]) return false }, undefined, $localize`Go to my videos`), new Hotkey('m i', (event: KeyboardEvent): boolean => { - this.router.navigate([ '/my-account/video-imports' ]) + this.router.navigate([ '/my-library/video-imports' ]) return false }, undefined, $localize`Go to my imports`), new Hotkey('m c', (event: KeyboardEvent): boolean => { - this.router.navigate([ '/my-account/video-channels' ]) + this.router.navigate([ '/my-library/video-channels' ]) return false }, undefined, $localize`Go to my channels`) ] diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 2011899d3..a4b4f26d0 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -1,98 +1,106 @@