From 67ed6552b831df66713bac9e672738796128d33f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:10:17 +0200 Subject: Reorganize client shared modules --- .../my-account-video-channel-create.component.ts | 82 +++++++++++ .../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 | 35 +++++ .../my-account-video-channels.component.scss | 115 +++++++++++++++ .../my-account-video-channels.component.ts | 154 +++++++++++++++++++++ .../my-account-video-channels.module.ts | 31 +++++ 10 files changed, 784 insertions(+) create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts create mode 100644 client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts (limited to 'client/src/app/+my-account/+my-account-video-channels') 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 new file mode 100644 index 000000000..039c389e4 --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts @@ -0,0 +1,82 @@ +import { Component, OnInit } from '@angular/core' +import { Router } from '@angular/router' +import { AuthService, Notifier } from '@app/core' +import { FormValidatorService, VideoChannelValidatorsService } from '@app/shared/shared-forms' +import { VideoChannelService } from '@app/shared/shared-main' +import { I18n } from '@ngx-translate/i18n-polyfill' +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 videoChannelValidatorsService: VideoChannelValidatorsService, + private notifier: Notifier, + private router: Router, + private videoChannelService: VideoChannelService, + private i18n: I18n + ) { + super() + } + + get instanceHost () { + return window.location.host + } + + ngOnInit () { + this.buildForm({ + name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME, + 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, + description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, + support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT + }) + } + + 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( + this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName }) + ) + this.router.navigate([ '/my-account', 'video-channels' ]) + }, + + err => { + if (err.status === 409) { + this.error = this.i18n('This name already exists on this instance.') + return + } + + this.error = err.message + } + ) + } + + isCreation () { + return true + } + + getFormButtonTitle () { + return this.i18n('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 new file mode 100644 index 000000000..048d143cd --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-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-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 new file mode 100644 index 000000000..8f8af655c --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-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-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 new file mode 100644 index 000000000..710c51d8e --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..489c437ea --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-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 { FormValidatorService, VideoChannelValidatorsService } from '@app/shared/shared-forms' +import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' +import { I18n } from '@ngx-translate/i18n-polyfill' +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 videoChannelValidatorsService: VideoChannelValidatorsService, + private notifier: Notifier, + private router: Router, + private route: ActivatedRoute, + private videoChannelService: VideoChannelService, + private i18n: I18n, + private serverService: ServerService + ) { + super() + } + + ngOnInit () { + this.serverConfig = this.serverService.getTmpConfig() + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.buildForm({ + 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, + description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, + support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT, + 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( + this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName }) + ) + + 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(this.i18n('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 this.i18n('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 new file mode 100644 index 000000000..94037e18f --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts @@ -0,0 +1,41 @@ +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: 'Account video channels' + } + } + }, + { + path: 'create', + component: MyAccountVideoChannelCreateComponent, + data: { + meta: { + title: 'Create new video channel' + } + } + }, + { + path: 'update/:videoChannelId', + component: MyAccountVideoChannelUpdateComponent, + data: { + meta: { + title: '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 new file mode 100644 index 000000000..b2e8210d3 --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html @@ -0,0 +1,35 @@ +

My channels

+
+ + + 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 new file mode 100644 index 000000000..76fb2cde0 --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss @@ -0,0 +1,115 @@ +@import '_variables'; +@import '_mixins'; + +.create-button { + @include create-button; +} + +::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-followers { + + } + } + } + + .video-channel-buttons { + margin-top: 10px; + min-width: 190px; + } +} + +.video-channels-header { + text-align: right; + margin: 20px 0 50px; +} + +::ng-deep .chartjs-render-monitor { + position: relative; + top: 1px; +} + +@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 (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 new file mode 100644 index 000000000..70510d7c9 --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts @@ -0,0 +1,154 @@ +import { ChartData } from 'chart.js' +import { max, maxBy, min, minBy } from 'lodash-es' +import { flatMap } 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' +import { I18n } from '@ngx-translate/i18n-polyfill' + +@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 { + videoChannels: VideoChannel[] = [] + videoChannelsChartData: ChartData[] + videoChannelsMinimumDailyViews = 0 + videoChannelsMaximumDailyViews: number + + private user: User + + constructor ( + private authService: AuthService, + private notifier: Notifier, + private confirmService: ConfirmService, + private videoChannelService: VideoChannelService, + private screenService: ScreenService, + private i18n: I18n + ) {} + + ngOnInit () { + this.user = this.authService.getUser() + + 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 + } + } + } + + async deleteVideoChannel (videoChannel: VideoChannel) { + const res = await this.confirmService.confirmWithInput( + this.i18n( + // tslint:disable + 'Do you really want to delete {{channelDisplayName}}? It will delete {{videosCount}} videos uploaded in this channel, and you will not be able to create another channel with the same name ({{channelName}})!', + { channelDisplayName: videoChannel.displayName, videosCount: videoChannel.videosCount, channelName: videoChannel.name } + ), + this.i18n( + 'Please type the display name of the video channel ({{displayName}}) to confirm', + { displayName: videoChannel.displayName } + ), + videoChannel.displayName, + this.i18n('Delete') + ) + if (res === false) return + + this.videoChannelService.removeVideoChannel(videoChannel) + .subscribe( + () => { + this.loadVideoChannels() + this.notifier.success( + this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName }) + ) + }, + + error => this.notifier.error(error.message) + ) + } + + private loadVideoChannels () { + this.authService.userInformationLoaded + .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) + .subscribe(res => { + this.videoChannels = res.data + + // chart data + this.videoChannelsChartData = this.videoChannels.map(v => ({ + labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), + datasets: [ + { + label: this.i18n('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( + this.videoChannels.map(v => minBy( // compute local minimum daily views for each channel, by their "views" attribute + v.viewsPerDay, + day => day.views + ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute + ) + this.videoChannelsMaximumDailyViews = max( + this.videoChannels.map(v => maxBy( // compute local maximum daily views for each channel, by their "views" attribute + 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 new file mode 100644 index 000000000..f8c6ad56b --- /dev/null +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-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 { 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 { } -- cgit v1.2.3