diff options
Diffstat (limited to 'client/src/app')
115 files changed, 1141 insertions, 335 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index 105bc12c3..379c0443e 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -9,8 +9,11 @@ | |||
9 | 9 | ||
10 | <div class="channel-avatar-row"> | 10 | <div class="channel-avatar-row"> |
11 | <my-actor-avatar | 11 | <my-actor-avatar |
12 | [channel]="videoChannel" [internalHref]="getVideoChannelLink(videoChannel)" | 12 | [channel]="videoChannel" |
13 | i18n-title title="See this video channel" | 13 | [internalHref]="getVideoChannelLink(videoChannel)" |
14 | i18n-title | ||
15 | title="See this video channel" | ||
16 | size="75" | ||
14 | ></my-actor-avatar> | 17 | ></my-actor-avatar> |
15 | 18 | ||
16 | <h2> | 19 | <h2> |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index be9e94f69..30b8098be 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss | |||
@@ -29,7 +29,6 @@ | |||
29 | grid-template-rows: auto 1fr; | 29 | grid-template-rows: auto 1fr; |
30 | 30 | ||
31 | my-actor-avatar { | 31 | my-actor-avatar { |
32 | @include actor-avatar-size(75px); | ||
33 | @include margin-right(15px); | 32 | @include margin-right(15px); |
34 | 33 | ||
35 | grid-column: 1; | 34 | grid-column: 1; |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 8362e6b7e..1544ad034 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <div class="account-info"> | 2 | <div class="account-info"> |
3 | 3 | ||
4 | <div class="account-avatar-row"> | 4 | <div class="account-avatar-row"> |
5 | <my-actor-avatar class="main-avatar" [account]="account"></my-actor-avatar> | 5 | <my-actor-avatar class="main-avatar" [account]="account" size="120"></my-actor-avatar> |
6 | 6 | ||
7 | <div> | 7 | <div> |
8 | <div class="section-label" i18n>ACCOUNT</div> | 8 | <div class="section-label" i18n>ACCOUNT</div> |
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index b8a957d1c..746549555 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -52,6 +52,14 @@ export class AdminComponent implements OnInit { | |||
52 | }) | 52 | }) |
53 | } | 53 | } |
54 | 54 | ||
55 | if (this.hasVideoCommentsRight()) { | ||
56 | overviewItems.children.push({ | ||
57 | label: $localize`Comments`, | ||
58 | routerLink: '/admin/comments', | ||
59 | iconName: 'message-circle' | ||
60 | }) | ||
61 | } | ||
62 | |||
55 | if (overviewItems.children.length !== 0) { | 63 | if (overviewItems.children.length !== 0) { |
56 | this.menuEntries.push(overviewItems) | 64 | this.menuEntries.push(overviewItems) |
57 | } | 65 | } |
@@ -104,14 +112,6 @@ export class AdminComponent implements OnInit { | |||
104 | }) | 112 | }) |
105 | } | 113 | } |
106 | 114 | ||
107 | if (this.hasVideoCommentsRight()) { | ||
108 | moderationItems.children.push({ | ||
109 | label: $localize`Video comments`, | ||
110 | routerLink: '/admin/moderation/video-comments/list', | ||
111 | iconName: 'message-circle' | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | if (this.hasAccountsBlocklistRight()) { | 115 | if (this.hasAccountsBlocklistRight()) { |
116 | moderationItems.children.push({ | 116 | moderationItems.children.push({ |
117 | label: $localize`Muted accounts`, | 117 | label: $localize`Muted accounts`, |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index c672fa280..366e29883 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -32,13 +32,13 @@ import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbo | |||
32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' | 32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' |
33 | import { AbuseListComponent, VideoBlockListComponent } from './moderation' | 33 | import { AbuseListComponent, VideoBlockListComponent } from './moderation' |
34 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' | 34 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' |
35 | import { VideoCommentListComponent } from './moderation/video-comment-list' | ||
36 | import { | 35 | import { |
37 | UserCreateComponent, | 36 | UserCreateComponent, |
38 | UserListComponent, | 37 | UserListComponent, |
39 | UserPasswordComponent, | 38 | UserPasswordComponent, |
40 | UserUpdateComponent, | 39 | UserUpdateComponent, |
41 | VideoAdminService, | 40 | VideoAdminService, |
41 | VideoCommentListComponent, | ||
42 | VideoListComponent | 42 | VideoListComponent |
43 | } from './overview' | 43 | } from './overview' |
44 | import { | 44 | import { |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index f2eaa3033..e3b6f8305 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -197,6 +197,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
197 | resolutions: {} | 197 | resolutions: {} |
198 | } | 198 | } |
199 | }, | 199 | }, |
200 | videoEditor: { | ||
201 | enabled: null | ||
202 | }, | ||
200 | autoBlacklist: { | 203 | autoBlacklist: { |
201 | videos: { | 204 | videos: { |
202 | ofUsers: { | 205 | ofUsers: { |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html index 1158f027b..2be855756 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html | |||
@@ -192,4 +192,29 @@ | |||
192 | 192 | ||
193 | </div> | 193 | </div> |
194 | </div> | 194 | </div> |
195 | |||
196 | <div class="form-row mt-2"> <!-- video editor grid --> | ||
197 | <div class="form-group col-12 col-lg-4 col-xl-3"> | ||
198 | <div i18n class="inner-form-title">VIDEO EDITOR</div> | ||
199 | <div i18n class="inner-form-description"> | ||
200 | Allows your users to edit their video (cut, add intro/outro, add a watermark etc) | ||
201 | </div> | ||
202 | </div> | ||
203 | |||
204 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | ||
205 | |||
206 | <ng-container formGroupName="videoEditor"> | ||
207 | <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> | ||
208 | <my-peertube-checkbox | ||
209 | inputName="videoEditorEnabled" formControlName="enabled" | ||
210 | i18n-labelText labelText="Enable video editor" | ||
211 | > | ||
212 | <ng-container ngProjectAs="description" *ngIf="!isTranscodingEnabled()"> | ||
213 | <span i18n>⚠️ You need to enable transcoding first to enable video editor</span> | ||
214 | </ng-container> | ||
215 | </my-peertube-checkbox> | ||
216 | </div> | ||
217 | </ng-container> | ||
218 | </div> | ||
219 | </div> | ||
195 | </ng-container> | 220 | </ng-container> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts index 3397c3dbd..948c10b69 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts | |||
@@ -71,6 +71,8 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
71 | } | 71 | } |
72 | 72 | ||
73 | private checkTranscodingFields () { | 73 | private checkTranscodingFields () { |
74 | const transcodingControl = this.form.get('transcoding.enabled') | ||
75 | const videoEditorControl = this.form.get('videoEditor.enabled') | ||
74 | const hlsControl = this.form.get('transcoding.hls.enabled') | 76 | const hlsControl = this.form.get('transcoding.hls.enabled') |
75 | const webtorrentControl = this.form.get('transcoding.webtorrent.enabled') | 77 | const webtorrentControl = this.form.get('transcoding.webtorrent.enabled') |
76 | 78 | ||
@@ -95,5 +97,12 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
95 | webtorrentControl.enable() | 97 | webtorrentControl.enable() |
96 | } | 98 | } |
97 | }) | 99 | }) |
100 | |||
101 | transcodingControl.valueChanges | ||
102 | .subscribe(newValue => { | ||
103 | if (newValue === false) { | ||
104 | videoEditorControl.setValue(false) | ||
105 | } | ||
106 | }) | ||
98 | } | 107 | } |
99 | } | 108 | } |
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html deleted file mode 100644 index feade0c26..000000000 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html +++ /dev/null | |||
@@ -1,56 +0,0 @@ | |||
1 | <p-table | ||
2 | [value]="blockedAccounts" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" | ||
4 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | ||
5 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
6 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted accounts" | ||
7 | > | ||
8 | <ng-template pTemplate="caption"> | ||
9 | <div class="caption"> | ||
10 | <div class="ml-auto"> | ||
11 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | ||
12 | </div> | ||
13 | </div> | ||
14 | </ng-template> | ||
15 | |||
16 | <ng-template pTemplate="header"> | ||
17 | <tr> | ||
18 | <th style="width: 150px;">Action</th> <!-- column for action buttons --> | ||
19 | <th style="width: calc(100% - 300px);" i18n>Account</th> | ||
20 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
21 | </tr> | ||
22 | </ng-template> | ||
23 | |||
24 | <ng-template pTemplate="body" let-accountBlock> | ||
25 | <tr> | ||
26 | <td class="action-cell"> | ||
27 | <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button> | ||
28 | </td> | ||
29 | |||
30 | <td> | ||
31 | <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | ||
32 | <div class="chip two-lines"> | ||
33 | <my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar> | ||
34 | <div> | ||
35 | {{ accountBlock.blockedAccount.displayName }} | ||
36 | <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> | ||
37 | </div> | ||
38 | </div> | ||
39 | </a> | ||
40 | </td> | ||
41 | |||
42 | <td>{{ accountBlock.createdAt | date: 'short' }}</td> | ||
43 | </tr> | ||
44 | </ng-template> | ||
45 | |||
46 | <ng-template pTemplate="emptymessage"> | ||
47 | <tr> | ||
48 | <td colspan="6"> | ||
49 | <div class="no-results"> | ||
50 | <ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container> | ||
51 | <ng-container *ngIf="!search" i18n>No account found.</ng-container> | ||
52 | </div> | ||
53 | </td> | ||
54 | </tr> | ||
55 | </ng-template> | ||
56 | </p-table> | ||
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 5c39ff366..1ad301039 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts | |||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router' | |||
2 | import { AbuseListComponent } from '@app/+admin/moderation/abuse-list' | 2 | import { AbuseListComponent } from '@app/+admin/moderation/abuse-list' |
3 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' | 3 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' |
4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' | 4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' |
5 | import { VideoCommentListComponent } from './video-comment-list' | ||
6 | import { UserRightGuard } from '@app/core' | 5 | import { UserRightGuard } from '@app/core' |
7 | import { UserRight } from '@shared/models' | 6 | import { UserRight } from '@shared/models' |
8 | 7 | ||
@@ -69,6 +68,7 @@ export const ModerationRoutes: Routes = [ | |||
69 | } | 68 | } |
70 | }, | 69 | }, |
71 | 70 | ||
71 | // We move this component in admin overview pages | ||
72 | { | 72 | { |
73 | path: 'video-comments', | 73 | path: 'video-comments', |
74 | redirectTo: 'video-comments/list', | 74 | redirectTo: 'video-comments/list', |
@@ -76,14 +76,8 @@ export const ModerationRoutes: Routes = [ | |||
76 | }, | 76 | }, |
77 | { | 77 | { |
78 | path: 'video-comments/list', | 78 | path: 'video-comments/list', |
79 | component: VideoCommentListComponent, | 79 | redirectTo: '/admin/comments/list', |
80 | canActivate: [ UserRightGuard ], | 80 | pathMatch: 'full' |
81 | data: { | ||
82 | userRight: UserRight.SEE_ALL_COMMENTS, | ||
83 | meta: { | ||
84 | title: $localize`Video comments` | ||
85 | } | ||
86 | } | ||
87 | }, | 81 | }, |
88 | 82 | ||
89 | { | 83 | { |
diff --git a/client/src/app/+admin/moderation/video-comment-list/index.ts b/client/src/app/+admin/overview/comments/index.ts index eb08b4177..c487f7a81 100644 --- a/client/src/app/+admin/moderation/video-comment-list/index.ts +++ b/client/src/app/+admin/overview/comments/index.ts | |||
@@ -1 +1,2 @@ | |||
1 | export * from './video-comment-list.component' | 1 | export * from './video-comment-list.component' |
2 | export * from './video-comment.routes' | ||
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html index 9bf23c21a..27a5d82ff 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html | |||
@@ -25,8 +25,10 @@ | |||
25 | </my-action-dropdown> | 25 | </my-action-dropdown> |
26 | </div> | 26 | </div> |
27 | 27 | ||
28 | <div class="ml-auto"> | 28 | <div class="ml-auto right-form"> |
29 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> | 29 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> |
30 | |||
31 | <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> | ||
30 | </div> | 32 | </div> |
31 | </div> | 33 | </div> |
32 | </ng-template> | 34 | </ng-template> |
@@ -66,7 +68,7 @@ | |||
66 | <td> | 68 | <td> |
67 | <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 69 | <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
68 | <div class="chip two-lines"> | 70 | <div class="chip two-lines"> |
69 | <my-actor-avatar [account]="videoComment.account"></my-actor-avatar> | 71 | <my-actor-avatar [account]="videoComment.account" size="32"></my-actor-avatar> |
70 | <div> | 72 | <div> |
71 | {{ videoComment.account.displayName }} | 73 | {{ videoComment.account.displayName }} |
72 | <span>{{ videoComment.by }}</span> | 74 | <span>{{ videoComment.by }}</span> |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss b/client/src/app/+admin/overview/comments/video-comment-list.component.scss index 3cf7b8db6..d7eb02142 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.scss | |||
@@ -45,6 +45,14 @@ my-global-icon { | |||
45 | } | 45 | } |
46 | } | 46 | } |
47 | 47 | ||
48 | .right-form { | ||
49 | display: flex; | ||
50 | |||
51 | > *:not(:last-child) { | ||
52 | @include margin-right(10px); | ||
53 | } | ||
54 | } | ||
55 | |||
48 | @media screen and (max-width: $primeng-breakpoint) { | 56 | @media screen and (max-width: $primeng-breakpoint) { |
49 | .video { | 57 | .video { |
50 | align-items: flex-start !important; | 58 | align-items: flex-start !important; |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index 25fe65133..f3f43a900 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -117,7 +117,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
117 | return this.selectedComments.length !== 0 | 117 | return this.selectedComments.length !== 0 |
118 | } | 118 | } |
119 | 119 | ||
120 | protected reloadData () { | 120 | reloadData () { |
121 | this.videoCommentService.getAdminVideoComments({ | 121 | this.videoCommentService.getAdminVideoComments({ |
122 | pagination: this.pagination, | 122 | pagination: this.pagination, |
123 | sort: this.sort, | 123 | sort: this.sort, |
diff --git a/client/src/app/+admin/overview/comments/video-comment.routes.ts b/client/src/app/+admin/overview/comments/video-comment.routes.ts new file mode 100644 index 000000000..f0bd440ad --- /dev/null +++ b/client/src/app/+admin/overview/comments/video-comment.routes.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import { Routes } from '@angular/router' | ||
2 | import { UserRightGuard } from '@app/core' | ||
3 | import { UserRight } from '@shared/models' | ||
4 | import { VideoCommentListComponent } from './video-comment-list.component' | ||
5 | |||
6 | export const commentRoutes: Routes = [ | ||
7 | { | ||
8 | path: 'comments', | ||
9 | canActivate: [ UserRightGuard ], | ||
10 | data: { | ||
11 | userRight: UserRight.SEE_ALL_COMMENTS | ||
12 | }, | ||
13 | children: [ | ||
14 | { | ||
15 | path: '', | ||
16 | redirectTo: 'list', | ||
17 | pathMatch: 'full' | ||
18 | }, | ||
19 | { | ||
20 | path: 'list', | ||
21 | component: VideoCommentListComponent, | ||
22 | data: { | ||
23 | meta: { | ||
24 | title: $localize`Comments list` | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | ] | ||
29 | } | ||
30 | ] | ||
diff --git a/client/src/app/+admin/overview/index.ts b/client/src/app/+admin/overview/index.ts index a9c46893f..111360734 100644 --- a/client/src/app/+admin/overview/index.ts +++ b/client/src/app/+admin/overview/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './comments' | ||
1 | export * from './users' | 2 | export * from './users' |
2 | export * from './videos' | 3 | export * from './videos' |
3 | export * from './overview.routes' | 4 | export * from './overview.routes' |
diff --git a/client/src/app/+admin/overview/overview.routes.ts b/client/src/app/+admin/overview/overview.routes.ts index 1e6686d16..72d6835d7 100644 --- a/client/src/app/+admin/overview/overview.routes.ts +++ b/client/src/app/+admin/overview/overview.routes.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | import { UsersRoutes } from './users' | 2 | import { commentRoutes } from './comments' |
3 | import { VideosRoutes } from './videos' | 3 | import { usersRoutes } from './users' |
4 | import { videosRoutes } from './videos' | ||
4 | 5 | ||
5 | export const OverviewRoutes: Routes = [ | 6 | export const OverviewRoutes: Routes = [ |
6 | ...UsersRoutes, | 7 | ...commentRoutes, |
7 | ...VideosRoutes | 8 | ...usersRoutes, |
9 | ...videosRoutes | ||
8 | ] | 10 | ] |
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html index 7eb89fea1..0662e3ac3 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.html +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html | |||
@@ -62,10 +62,10 @@ | |||
62 | </div> | 62 | </div> |
63 | </th> | 63 | </th> |
64 | <th *ngIf="isSelected('username')" pResizableColumn pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th> | 64 | <th *ngIf="isSelected('username')" pResizableColumn pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th> |
65 | <th *ngIf="isSelected('role')" style="width: 120px;" pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th> | ||
65 | <th *ngIf="isSelected('email')">{{ getColumn('email').label }}</th> | 66 | <th *ngIf="isSelected('email')">{{ getColumn('email').label }}</th> |
66 | <th *ngIf="isSelected('quota')" style="width: 160px;" pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th> | 67 | <th *ngIf="isSelected('quota')" style="width: 160px;" pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th> |
67 | <th *ngIf="isSelected('quotaDaily')" style="width: 160px;">{{ getColumn('quotaDaily').label }}</th> | 68 | <th *ngIf="isSelected('quotaDaily')" style="width: 160px;">{{ getColumn('quotaDaily').label }}</th> |
68 | <th *ngIf="isSelected('role')" style="width: 120px;" pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th> | ||
69 | <th *ngIf="isSelected('pluginAuth')" style="width: 140px;" pResizableColumn >{{ getColumn('pluginAuth').label }}</th> | 69 | <th *ngIf="isSelected('pluginAuth')" style="width: 140px;" pResizableColumn >{{ getColumn('pluginAuth').label }}</th> |
70 | <th *ngIf="isSelected('createdAt')" style="width: 150px;" pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th> | 70 | <th *ngIf="isSelected('createdAt')" style="width: 150px;" pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th> |
71 | <th *ngIf="isSelected('lastLoginDate')" style="width: 150px;" pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th> | 71 | <th *ngIf="isSelected('lastLoginDate')" style="width: 150px;" pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th> |
@@ -84,8 +84,9 @@ | |||
84 | </td> | 84 | </td> |
85 | 85 | ||
86 | <td class="action-cell"> | 86 | <td class="action-cell"> |
87 | <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" container="body" | 87 | <my-user-moderation-dropdown |
88 | (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> | 88 | *ngIf="!isInSelectionMode()" [user]="user" [account]="user.accountMutedStatus" [displayOptions]="userModerationDisplayOptions" |
89 | container="body" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> | ||
89 | </my-user-moderation-dropdown> | 90 | </my-user-moderation-dropdown> |
90 | </td> | 91 | </td> |
91 | 92 | ||
@@ -99,6 +100,14 @@ | |||
99 | </div> | 100 | </div> |
100 | </div> | 101 | </div> |
101 | </a> | 102 | </a> |
103 | |||
104 | <div *ngIf="user.accountMutedStatus.mutedByInstance" class="badges-username badge badge-red" i18n>Muted</div> | ||
105 | <div *ngIf="user.blocked" class="badges-username badge badge-red" i18n>Banned</div> | ||
106 | </td> | ||
107 | |||
108 | <td *ngIf="isSelected('role')"> | ||
109 | <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> | ||
110 | <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> | ||
102 | </td> | 111 | </td> |
103 | 112 | ||
104 | <td *ngIf="isSelected('email')" [title]="user.email"> | 113 | <td *ngIf="isSelected('email')" [title]="user.email"> |
@@ -138,11 +147,6 @@ | |||
138 | </div> | 147 | </div> |
139 | </td> | 148 | </td> |
140 | 149 | ||
141 | <td *ngIf="isSelected('role')"> | ||
142 | <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> | ||
143 | <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> | ||
144 | </td> | ||
145 | |||
146 | <td *ngIf="isSelected('pluginAuth')"> | 150 | <td *ngIf="isSelected('pluginAuth')"> |
147 | <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container> | 151 | <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container> |
148 | </td> | 152 | </td> |
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss index 335bd2995..8160703f0 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss | |||
@@ -23,6 +23,10 @@ tr.banned > td { | |||
23 | font-weight: $font-semibold; | 23 | font-weight: $font-semibold; |
24 | } | 24 | } |
25 | 25 | ||
26 | .badges-username { | ||
27 | margin-left: 15px; | ||
28 | } | ||
29 | |||
26 | .user-table-primary-text .glyphicon { | 30 | .user-table-primary-text .glyphicon { |
27 | @include margin-left(0.1rem); | 31 | @include margin-left(0.1rem); |
28 | 32 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 9a9d0f5c6..d22e1355e 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -2,9 +2,10 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { getAPIHost } from '@app/helpers' | ||
5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
6 | import { DropdownAction } from '@app/shared/shared-main' | 7 | import { Actor, DropdownAction } from '@app/shared/shared-main' |
7 | import { UserBanModalComponent } from '@app/shared/shared-moderation' | 8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' |
8 | import { UserAdminService } from '@app/shared/shared-users' | 9 | import { UserAdminService } from '@app/shared/shared-users' |
9 | import { User, UserRole } from '@shared/models' | 10 | import { User, UserRole } from '@shared/models' |
10 | 11 | ||
@@ -23,7 +24,7 @@ type UserForList = User & { | |||
23 | export class UserListComponent extends RestTable implements OnInit { | 24 | export class UserListComponent extends RestTable implements OnInit { |
24 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent | 25 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent |
25 | 26 | ||
26 | users: User[] = [] | 27 | users: (User & { accountMutedStatus: AccountMutedStatus })[] = [] |
27 | 28 | ||
28 | totalRecords = 0 | 29 | totalRecords = 0 |
29 | sort: SortMeta = { field: 'createdAt', order: 1 } | 30 | sort: SortMeta = { field: 'createdAt', order: 1 } |
@@ -47,6 +48,12 @@ export class UserListComponent extends RestTable implements OnInit { | |||
47 | } | 48 | } |
48 | ] | 49 | ] |
49 | 50 | ||
51 | userModerationDisplayOptions: UserModerationDisplayType = { | ||
52 | instanceAccount: true, | ||
53 | instanceUser: true, | ||
54 | myAccount: false | ||
55 | } | ||
56 | |||
50 | requiresEmailVerification = false | 57 | requiresEmailVerification = false |
51 | 58 | ||
52 | private _selectedColumns: string[] | 59 | private _selectedColumns: string[] |
@@ -58,6 +65,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
58 | private confirmService: ConfirmService, | 65 | private confirmService: ConfirmService, |
59 | private serverService: ServerService, | 66 | private serverService: ServerService, |
60 | private auth: AuthService, | 67 | private auth: AuthService, |
68 | private blocklist: BlocklistService, | ||
61 | private userAdminService: UserAdminService | 69 | private userAdminService: UserAdminService |
62 | ) { | 70 | ) { |
63 | super() | 71 | super() |
@@ -115,9 +123,9 @@ export class UserListComponent extends RestTable implements OnInit { | |||
115 | 123 | ||
116 | this.columns = [ | 124 | this.columns = [ |
117 | { id: 'username', label: $localize`Username` }, | 125 | { id: 'username', label: $localize`Username` }, |
126 | { id: 'role', label: $localize`Role` }, | ||
118 | { id: 'email', label: $localize`Email` }, | 127 | { id: 'email', label: $localize`Email` }, |
119 | { id: 'quota', label: $localize`Video quota` }, | 128 | { id: 'quota', label: $localize`Video quota` }, |
120 | { id: 'role', label: $localize`Role` }, | ||
121 | { id: 'createdAt', label: $localize`Created` } | 129 | { id: 'createdAt', label: $localize`Created` } |
122 | ] | 130 | ] |
123 | 131 | ||
@@ -237,11 +245,35 @@ export class UserListComponent extends RestTable implements OnInit { | |||
237 | search: this.search | 245 | search: this.search |
238 | }).subscribe({ | 246 | }).subscribe({ |
239 | next: resultList => { | 247 | next: resultList => { |
240 | this.users = resultList.data | 248 | this.users = resultList.data.map(u => ({ |
249 | ...u, | ||
250 | |||
251 | accountMutedStatus: { | ||
252 | ...u.account, | ||
253 | |||
254 | nameWithHost: Actor.CREATE_BY_STRING(u.account.name, u.account.host), | ||
255 | |||
256 | mutedByInstance: false, | ||
257 | mutedByUser: false, | ||
258 | mutedServerByInstance: false, | ||
259 | mutedServerByUser: false | ||
260 | } | ||
261 | })) | ||
241 | this.totalRecords = resultList.total | 262 | this.totalRecords = resultList.total |
263 | |||
264 | this.loadMutedStatus() | ||
242 | }, | 265 | }, |
243 | 266 | ||
244 | error: err => this.notifier.error(err.message) | 267 | error: err => this.notifier.error(err.message) |
245 | }) | 268 | }) |
246 | } | 269 | } |
270 | |||
271 | private loadMutedStatus () { | ||
272 | this.blocklist.getStatus({ accounts: this.users.map(u => u.username + '@' + getAPIHost()) }) | ||
273 | .subscribe(blockStatus => { | ||
274 | for (const user of this.users) { | ||
275 | user.accountMutedStatus.mutedByInstance = blockStatus.accounts[user.username + '@' + getAPIHost()].blockedByServer | ||
276 | } | ||
277 | }) | ||
278 | } | ||
247 | } | 279 | } |
diff --git a/client/src/app/+admin/overview/users/users.routes.ts b/client/src/app/+admin/overview/users/users.routes.ts index 8b63f5bc7..c9724e5fb 100644 --- a/client/src/app/+admin/overview/users/users.routes.ts +++ b/client/src/app/+admin/overview/users/users.routes.ts | |||
@@ -4,7 +4,7 @@ import { UserRight } from '@shared/models' | |||
4 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' | 4 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' |
5 | import { UserListComponent } from './user-list' | 5 | import { UserListComponent } from './user-list' |
6 | 6 | ||
7 | export const UsersRoutes: Routes = [ | 7 | export const usersRoutes: Routes = [ |
8 | { | 8 | { |
9 | path: 'users', | 9 | path: 'users', |
10 | canActivate: [ UserRightGuard ], | 10 | canActivate: [ UserRightGuard ], |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html index 974912b32..75d9be5f1 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -24,9 +24,7 @@ | |||
24 | <div class="ml-auto right-form"> | 24 | <div class="ml-auto right-form"> |
25 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> | 25 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> |
26 | 26 | ||
27 | <div class="button-filter-block"> | 27 | <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> |
28 | <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> | ||
29 | </div> | ||
30 | </div> | 28 | </div> |
31 | 29 | ||
32 | </div> | 30 | </div> |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.scss b/client/src/app/+admin/overview/videos/video-list.component.scss index 543cb433c..cb47b6548 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.scss +++ b/client/src/app/+admin/overview/videos/video-list.component.scss | |||
@@ -1,5 +1,6 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | |||
3 | my-embed { | 4 | my-embed { |
4 | display: block; | 5 | display: block; |
5 | max-width: 500px; | 6 | max-width: 500px; |
@@ -27,4 +28,3 @@ my-embed { | |||
27 | @include margin-right(10px); | 28 | @include margin-right(10px); |
28 | } | 29 | } |
29 | } | 30 | } |
30 | |||
diff --git a/client/src/app/+admin/overview/videos/video.routes.ts b/client/src/app/+admin/overview/videos/video.routes.ts index 984df7b82..01cb5b497 100644 --- a/client/src/app/+admin/overview/videos/video.routes.ts +++ b/client/src/app/+admin/overview/videos/video.routes.ts | |||
@@ -3,7 +3,7 @@ import { UserRightGuard } from '@app/core' | |||
3 | import { UserRight } from '@shared/models' | 3 | import { UserRight } from '@shared/models' |
4 | import { VideoListComponent } from './video-list.component' | 4 | import { VideoListComponent } from './video-list.component' |
5 | 5 | ||
6 | export const VideosRoutes: Routes = [ | 6 | export const videosRoutes: Routes = [ |
7 | { | 7 | { |
8 | path: 'videos', | 8 | path: 'videos', |
9 | canActivate: [ UserRightGuard ], | 9 | canActivate: [ UserRightGuard ], |
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts index 21b6167b2..51cd45605 100644 --- a/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts +++ b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts | |||
@@ -111,7 +111,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI | |||
111 | next: data => { | 111 | next: data => { |
112 | this.notifier.success($localize`Avatar changed.`) | 112 | this.notifier.success($localize`Avatar changed.`) |
113 | 113 | ||
114 | this.videoChannel.updateAvatar(data.avatar) | 114 | this.videoChannel.updateAvatar(data.avatars) |
115 | }, | 115 | }, |
116 | 116 | ||
117 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ | 117 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ |
@@ -141,7 +141,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI | |||
141 | next: data => { | 141 | next: data => { |
142 | this.notifier.success($localize`Banner changed.`) | 142 | this.notifier.success($localize`Banner changed.`) |
143 | 143 | ||
144 | this.videoChannel.updateBanner(data.banner) | 144 | this.videoChannel.updateBanner(data.banners) |
145 | }, | 145 | }, |
146 | 146 | ||
147 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ | 147 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index a5bcb6496..577f4a252 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -43,7 +43,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
43 | next: data => { | 43 | next: data => { |
44 | this.notifier.success($localize`Avatar changed.`) | 44 | this.notifier.success($localize`Avatar changed.`) |
45 | 45 | ||
46 | this.user.updateAccountAvatar(data.avatar) | 46 | this.user.updateAccountAvatar(data.avatars) |
47 | }, | 47 | }, |
48 | 48 | ||
49 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ | 49 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ |
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 index 77947315b..c1ded0f6d 100644 --- 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 | |||
@@ -19,7 +19,7 @@ | |||
19 | 19 | ||
20 | <div class="video-channels"> | 20 | <div class="video-channels"> |
21 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> | 21 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> |
22 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar> | 22 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar> |
23 | 23 | ||
24 | <div class="video-channel-info"> | 24 | <div class="video-channel-info"> |
25 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> | 25 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> |
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 index 998e46cb2..484355967 100644 --- 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 | |||
@@ -24,7 +24,6 @@ my-edit-button { | |||
24 | padding-bottom: 0; | 24 | padding-bottom: 0; |
25 | 25 | ||
26 | my-actor-avatar { | 26 | my-actor-avatar { |
27 | @include actor-avatar-size(80px); | ||
28 | @include margin-right(10px); | 27 | @include margin-right(10px); |
29 | } | 28 | } |
30 | } | 29 | } |
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.html b/client/src/app/+my-library/my-follows/my-followers.component.html index eac750c86..4303695a3 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.html +++ b/client/src/app/+my-library/my-follows/my-followers.component.html | |||
@@ -14,7 +14,7 @@ | |||
14 | 14 | ||
15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
16 | <div *ngFor="let follow of follows" class="actor"> | 16 | <div *ngFor="let follow of follows" class="actor"> |
17 | <my-actor-avatar [account]="follow.follower" [href]="follow.follower.url"></my-actor-avatar> | 17 | <my-actor-avatar [account]="follow.follower" [href]="follow.follower.url" size="40"></my-actor-avatar> |
18 | 18 | ||
19 | <div class="actor-info"> | 19 | <div class="actor-info"> |
20 | <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page"> | 20 | <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page"> |
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.scss b/client/src/app/+my-library/my-follows/my-followers.component.scss index 15b51c419..fae4cd972 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.scss +++ b/client/src/app/+my-library/my-follows/my-followers.component.scss | |||
@@ -12,7 +12,7 @@ input[type=text] { | |||
12 | } | 12 | } |
13 | 13 | ||
14 | .actor { | 14 | .actor { |
15 | @include actor-row($avatar-size: 40px, $min-height: auto, $separator: true); | 15 | @include actor-row($min-height: auto, $separator: true); |
16 | 16 | ||
17 | .actor-display-name { | 17 | .actor-display-name { |
18 | font-size: 16px; | 18 | font-size: 16px; |
diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.html b/client/src/app/+my-library/my-follows/my-subscriptions.component.html index 775f0e783..391c4d3be 100644 --- a/client/src/app/+my-library/my-follows/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.html | |||
@@ -14,7 +14,7 @@ | |||
14 | 14 | ||
15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
16 | <div *ngFor="let videoChannel of videoChannels" class="actor"> | 16 | <div *ngFor="let videoChannel of videoChannels" class="actor"> |
17 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar> | 17 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar> |
18 | 18 | ||
19 | <div class="actor-info"> | 19 | <div class="actor-info"> |
20 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page"> | 20 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page"> |
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 index 764da2369..8ead237c7 100644 --- 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 | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 1 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { listUserChannels } from '@app/helpers' | 4 | import { listUserChannelsForSelect } from '@app/helpers' |
5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | 5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
7 | import { VideoOwnershipService } from '@app/shared/shared-main' | 7 | import { VideoOwnershipService } from '@app/shared/shared-main' |
@@ -36,7 +36,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
36 | ngOnInit () { | 36 | ngOnInit () { |
37 | this.videoChannels = [] | 37 | this.videoChannels = [] |
38 | 38 | ||
39 | listUserChannels(this.authService) | 39 | listUserChannelsForSelect(this.authService) |
40 | .subscribe(channels => this.videoChannels = channels) | 40 | .subscribe(channels => this.videoChannels = channels) |
41 | 41 | ||
42 | this.buildForm({ | 42 | this.buildForm({ |
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 index 4c02c78fc..cb032505e 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.html +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.html | |||
@@ -37,7 +37,7 @@ | |||
37 | <td> | 37 | <td> |
38 | <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 38 | <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
39 | <div class="chip two-lines"> | 39 | <div class="chip two-lines"> |
40 | <my-actor-avatar [account]="videoChangeOwnership.initiatorAccount"></my-actor-avatar> | 40 | <my-actor-avatar [account]="videoChangeOwnership.initiatorAccount" size="32"></my-actor-avatar> |
41 | <div> | 41 | <div> |
42 | {{ videoChangeOwnership.initiatorAccount.displayName }} | 42 | {{ videoChangeOwnership.initiatorAccount.displayName }} |
43 | <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> | 43 | <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> |
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 index 8bc78b2db..9eb3e9888 100644 --- 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 | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { listUserChannels } from '@app/helpers' | 4 | import { listUserChannelsForSelect } from '@app/helpers' |
5 | import { | 5 | import { |
6 | setPlaylistChannelValidator, | 6 | setPlaylistChannelValidator, |
7 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | 7 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, |
@@ -46,7 +46,7 @@ export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implemen | |||
46 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) | 46 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) |
47 | }) | 47 | }) |
48 | 48 | ||
49 | listUserChannels(this.authService) | 49 | listUserChannelsForSelect(this.authService) |
50 | .subscribe(channels => this.userVideoChannels = channels) | 50 | .subscribe(channels => this.userVideoChannels = channels) |
51 | 51 | ||
52 | this.serverService.getVideoPlaylistPrivacies() | 52 | this.serverService.getVideoPlaylistPrivacies() |
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 index 06ac3ad50..ef7ba0018 100644 --- 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 | |||
@@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators' | |||
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService } from '@app/core' |
6 | import { listUserChannels } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { | 7 | import { |
8 | setPlaylistChannelValidator, | 8 | setPlaylistChannelValidator, |
9 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | 9 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, |
@@ -51,7 +51,7 @@ export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implemen | |||
51 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) | 51 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) |
52 | }) | 52 | }) |
53 | 53 | ||
54 | listUserChannels(this.authService) | 54 | listUserChannelsForSelect(this.authService) |
55 | .subscribe(channels => this.userVideoChannels = channels) | 55 | .subscribe(channels => this.userVideoChannels = channels) |
56 | 56 | ||
57 | this.paramsSub = this.route.params | 57 | this.paramsSub = this.route.params |
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 index 261e87f99..c998b7c49 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -9,7 +9,7 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms' | |||
9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
10 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 10 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
11 | import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' | 11 | import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' |
12 | import { VideoChannel, VideoSortField } from '@shared/models' | 12 | import { VideoChannel, VideoSortField, VideoState } from '@shared/models' |
13 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' | 13 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' |
14 | 14 | ||
15 | @Component({ | 15 | @Component({ |
@@ -205,6 +205,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
205 | private buildActions () { | 205 | private buildActions () { |
206 | this.videoActions = [ | 206 | this.videoActions = [ |
207 | { | 207 | { |
208 | label: $localize`Editor`, | ||
209 | linkBuilder: ({ video }) => [ '/video-editor/edit', video.uuid ], | ||
210 | isDisplayed: ({ video }) => video.state.id === VideoState.PUBLISHED, | ||
211 | iconName: 'film' | ||
212 | }, | ||
213 | { | ||
208 | label: $localize`Display live information`, | 214 | label: $localize`Display live information`, |
209 | handler: ({ video }) => this.displayLiveInformation(video), | 215 | handler: ({ video }) => this.displayLiveInformation(video), |
210 | isDisplayed: ({ video }) => video.isLive, | 216 | isDisplayed: ({ video }) => video.isLive, |
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index 412b962d1..2c84dd930 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html | |||
@@ -36,7 +36,7 @@ | |||
36 | <ng-container *ngFor="let result of results"> | 36 | <ng-container *ngFor="let result of results"> |
37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> | 37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> |
38 | 38 | ||
39 | <my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)"></my-actor-avatar> | 39 | <my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)" size="120"></my-actor-avatar> |
40 | 40 | ||
41 | <div class="video-channel-info"> | 41 | <div class="video-channel-info"> |
42 | <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names"> | 42 | <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names"> |
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss index b521825e5..cab1d0e88 100644 --- a/client/src/app/+search/search.component.scss +++ b/client/src/app/+search/search.component.scss | |||
@@ -58,10 +58,6 @@ | |||
58 | max-width: 800px; | 58 | max-width: 800px; |
59 | } | 59 | } |
60 | 60 | ||
61 | .video-channel my-actor-avatar { | ||
62 | @include build-channel-img-size($video-thumbnail-width); | ||
63 | } | ||
64 | |||
65 | .video-channel-info { | 61 | .video-channel-info { |
66 | flex-grow: 1; | 62 | flex-grow: 1; |
67 | margin: 0 10px; | 63 | margin: 0 10px; |
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 212e2f867..09f81d2ce 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -23,7 +23,7 @@ | |||
23 | <div class="section-label" i18n>OWNER ACCOUNT</div> | 23 | <div class="section-label" i18n>OWNER ACCOUNT</div> |
24 | 24 | ||
25 | <div class="avatar-row"> | 25 | <div class="avatar-row"> |
26 | <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar> | 26 | <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()" size="48"></my-actor-avatar> |
27 | 27 | ||
28 | <div class="actor-info"> | 28 | <div class="actor-info"> |
29 | <h4> | 29 | <h4> |
@@ -51,7 +51,7 @@ | |||
51 | </ng-template> | 51 | </ng-template> |
52 | 52 | ||
53 | <div class="channel-avatar-row"> | 53 | <div class="channel-avatar-row"> |
54 | <my-actor-avatar class="main-avatar" [channel]="videoChannel"></my-actor-avatar> | 54 | <my-actor-avatar class="main-avatar" [channel]="videoChannel" size="120"></my-actor-avatar> |
55 | 55 | ||
56 | <div> | 56 | <div> |
57 | <div class="section-label" i18n>VIDEO CHANNEL</div> | 57 | <div class="section-label" i18n>VIDEO CHANNEL</div> |
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 72ee2d7bb..004ad7998 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -107,10 +107,6 @@ | |||
107 | display: flex; | 107 | display: flex; |
108 | margin-bottom: 15px; | 108 | margin-bottom: 15px; |
109 | 109 | ||
110 | .account-avatar { | ||
111 | @include actor-avatar-size(48px); | ||
112 | } | ||
113 | |||
114 | .actor-info { | 110 | .actor-info { |
115 | @include margin-left(15px); | 111 | @include margin-left(15px); |
116 | } | 112 | } |
diff --git a/client/src/app/+video-editor/edit/index.ts b/client/src/app/+video-editor/edit/index.ts new file mode 100644 index 000000000..390ca80fc --- /dev/null +++ b/client/src/app/+video-editor/edit/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-editor-edit.component' | ||
2 | export * from './video-editor-edit.resolver' | ||
diff --git a/client/src/app/+video-editor/edit/video-editor-edit.component.html b/client/src/app/+video-editor/edit/video-editor-edit.component.html new file mode 100644 index 000000000..d33dfaf18 --- /dev/null +++ b/client/src/app/+video-editor/edit/video-editor-edit.component.html | |||
@@ -0,0 +1,88 @@ | |||
1 | <div class="margin-content"> | ||
2 | <h1 class="title-page title-page-single" i18n>Edit {{ video.name }}</h1> | ||
3 | |||
4 | <div class="columns"> | ||
5 | <form role="form" [formGroup]="form"> | ||
6 | |||
7 | <div class="section cut" formGroupName="cut"> | ||
8 | <h2 i18n>CUT VIDEO</h2> | ||
9 | |||
10 | <div i18n class="description">Set a new start/end.</div> | ||
11 | |||
12 | <div class="form-group"> | ||
13 | <label i18n for="cutStart">New start</label> | ||
14 | <my-timestamp-input inputName="cutStart" [disableBorder]="false" [maxTimestamp]="video.duration" formControlName="start"></my-timestamp-input> | ||
15 | </div> | ||
16 | |||
17 | <div class="form-group"> | ||
18 | <label i18n for="cutEnd">New end</label> | ||
19 | <my-timestamp-input inputName="cutEnd" [disableBorder]="false" [maxTimestamp]="video.duration" formControlName="end"></my-timestamp-input> | ||
20 | </div> | ||
21 | </div> | ||
22 | |||
23 | <div class="section" formGroupName="add-intro"> | ||
24 | <h2 i18n>ADD INTRO</h2> | ||
25 | |||
26 | <div i18n class="description">Concatenate a file at the beginning of the video.</div> | ||
27 | |||
28 | <div class="form-group"> | ||
29 | <my-reactive-file | ||
30 | formControlName="file" inputName="addIntroFile" i18n-inputLabel inputLabel="Select the intro video file" | ||
31 | [extensions]="videoExtensions" [displayFilename]="true" | ||
32 | [ngbTooltip]="getIntroOutroTooltip()" | ||
33 | ></my-reactive-file> | ||
34 | </div> | ||
35 | </div> | ||
36 | |||
37 | <div class="section" formGroupName="add-outro"> | ||
38 | <h2 i18n>ADD OUTRO</h2> | ||
39 | |||
40 | <div i18n class="description">Concatenate a file at the end of the video.</div> | ||
41 | |||
42 | <div class="form-group"> | ||
43 | <my-reactive-file | ||
44 | formControlName="file" inputName="addOutroFile" i18n-inputLabel inputLabel="Select the outro video file" | ||
45 | [extensions]="videoExtensions" [displayFilename]="true" | ||
46 | [ngbTooltip]="getIntroOutroTooltip()" | ||
47 | ></my-reactive-file> | ||
48 | </div> | ||
49 | </div> | ||
50 | |||
51 | <div class="section" formGroupName="add-watermark"> | ||
52 | <h2 i18n>ADD WATERMARK</h2> | ||
53 | |||
54 | <div i18n class="description">Add a watermark image to the video.</div> | ||
55 | |||
56 | <div class="form-group"> | ||
57 | <my-reactive-file | ||
58 | formControlName="file" inputName="addWatermarkFile" i18n-inputLabel inputLabel="Select watermark image file" | ||
59 | [extensions]="imageExtensions" [displayFilename]="true" | ||
60 | [ngbTooltip]="getWatermarkTooltip()" | ||
61 | ></my-reactive-file> | ||
62 | </div> | ||
63 | </div> | ||
64 | |||
65 | <my-button | ||
66 | className="orange-button" i18n-label label="Run video edition" icon="circle-tick" | ||
67 | (click)="runEdition()" (keydown.enter)="runEdition()" | ||
68 | [disabled]="!form.valid || isRunningEdition || noEdition()" | ||
69 | ></my-button> | ||
70 | </form> | ||
71 | |||
72 | |||
73 | <div class="information"> | ||
74 | <div> | ||
75 | <label i18n>Video before edition</label> | ||
76 | <my-embed [video]="video"></my-embed> | ||
77 | </div> | ||
78 | |||
79 | <div *ngIf="!noEdition()"> | ||
80 | <label i18n>Edition tasks:</label> | ||
81 | |||
82 | <ol> | ||
83 | <li *ngFor="let task of getTasksSummary()">{{ task }}</li> | ||
84 | </ol> | ||
85 | </div> | ||
86 | </div> | ||
87 | </div> | ||
88 | </div> | ||
diff --git a/client/src/app/+video-editor/edit/video-editor-edit.component.scss b/client/src/app/+video-editor/edit/video-editor-edit.component.scss new file mode 100644 index 000000000..43f336f59 --- /dev/null +++ b/client/src/app/+video-editor/edit/video-editor-edit.component.scss | |||
@@ -0,0 +1,76 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | .columns { | ||
5 | display: flex; | ||
6 | |||
7 | .information { | ||
8 | width: 100%; | ||
9 | margin-left: 50px; | ||
10 | |||
11 | > div { | ||
12 | margin-bottom: 30px; | ||
13 | } | ||
14 | |||
15 | @media screen and (max-width: $small-view) { | ||
16 | display: none; | ||
17 | } | ||
18 | } | ||
19 | } | ||
20 | |||
21 | h1 { | ||
22 | font-size: 20px; | ||
23 | } | ||
24 | |||
25 | h2 { | ||
26 | font-weight: $font-bold; | ||
27 | font-size: 16px; | ||
28 | color: pvar(--mainColor); | ||
29 | background-color: pvar(--mainBackgroundColor); | ||
30 | padding: 0 5px; | ||
31 | width: fit-content; | ||
32 | margin: -8px 0 0; | ||
33 | } | ||
34 | |||
35 | .section { | ||
36 | $min-width: 600px; | ||
37 | |||
38 | @include padding-left(10px); | ||
39 | |||
40 | min-width: $min-width; | ||
41 | |||
42 | margin-bottom: 50px; | ||
43 | border: 1px solid $separator-border-color; | ||
44 | border-radius: 5px; | ||
45 | width: fit-content; | ||
46 | |||
47 | .form-group, | ||
48 | .description { | ||
49 | @include margin-left(5px); | ||
50 | } | ||
51 | |||
52 | .description { | ||
53 | color: pvar(--greyForegroundColor); | ||
54 | margin-top: 5px; | ||
55 | margin-bottom: 15px; | ||
56 | } | ||
57 | |||
58 | @media screen and (max-width: $min-width) { | ||
59 | min-width: none; | ||
60 | } | ||
61 | } | ||
62 | |||
63 | my-timestamp-input { | ||
64 | display: block; | ||
65 | } | ||
66 | |||
67 | my-embed { | ||
68 | display: block; | ||
69 | max-width: 500px; | ||
70 | width: 100%; | ||
71 | } | ||
72 | |||
73 | my-reactive-file { | ||
74 | display: block; | ||
75 | width: fit-content; | ||
76 | } | ||
diff --git a/client/src/app/+video-editor/edit/video-editor-edit.component.ts b/client/src/app/+video-editor/edit/video-editor-edit.component.ts new file mode 100644 index 000000000..93d7ffcec --- /dev/null +++ b/client/src/app/+video-editor/edit/video-editor-edit.component.ts | |||
@@ -0,0 +1,202 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { ConfirmService, Notifier, ServerService } from '@app/core' | ||
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
5 | import { Video, VideoDetails } from '@app/shared/shared-main' | ||
6 | import { LoadingBarService } from '@ngx-loading-bar/core' | ||
7 | import { secondsToTime } from '@shared/core-utils' | ||
8 | import { VideoEditorTask, VideoEditorTaskCut } from '@shared/models' | ||
9 | import { VideoEditorService } from '../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-video-editor-edit', | ||
13 | templateUrl: './video-editor-edit.component.html', | ||
14 | styleUrls: [ './video-editor-edit.component.scss' ] | ||
15 | }) | ||
16 | export class VideoEditorEditComponent extends FormReactive implements OnInit { | ||
17 | isRunningEdition = false | ||
18 | |||
19 | video: VideoDetails | ||
20 | |||
21 | constructor ( | ||
22 | protected formValidatorService: FormValidatorService, | ||
23 | private serverService: ServerService, | ||
24 | private notifier: Notifier, | ||
25 | private router: Router, | ||
26 | private route: ActivatedRoute, | ||
27 | private videoEditorService: VideoEditorService, | ||
28 | private loadingBar: LoadingBarService, | ||
29 | private confirmService: ConfirmService | ||
30 | ) { | ||
31 | super() | ||
32 | } | ||
33 | |||
34 | ngOnInit () { | ||
35 | this.video = this.route.snapshot.data.video | ||
36 | |||
37 | const defaultValues = { | ||
38 | cut: { | ||
39 | start: 0, | ||
40 | end: this.video.duration | ||
41 | } | ||
42 | } | ||
43 | |||
44 | this.buildForm({ | ||
45 | cut: { | ||
46 | start: null, | ||
47 | end: null | ||
48 | }, | ||
49 | 'add-intro': { | ||
50 | file: null | ||
51 | }, | ||
52 | 'add-outro': { | ||
53 | file: null | ||
54 | }, | ||
55 | 'add-watermark': { | ||
56 | file: null | ||
57 | } | ||
58 | }, defaultValues) | ||
59 | } | ||
60 | |||
61 | get videoExtensions () { | ||
62 | return this.serverService.getHTMLConfig().video.file.extensions | ||
63 | } | ||
64 | |||
65 | get imageExtensions () { | ||
66 | return this.serverService.getHTMLConfig().video.image.extensions | ||
67 | } | ||
68 | |||
69 | async runEdition () { | ||
70 | if (this.isRunningEdition) return | ||
71 | |||
72 | const title = $localize`Are you sure you want to edit "${this.video.name}"?` | ||
73 | const listHTML = this.getTasksSummary().map(t => `<li>${t}</li>`).join('') | ||
74 | |||
75 | // eslint-disable-next-line max-len | ||
76 | const confirmHTML = $localize`The current video will be overwritten by this edited video and <strong>you won't be able to recover it</strong>.<br /><br />` + | ||
77 | $localize`As a reminder, the following tasks will be executed: <ol>${listHTML}</ol>` | ||
78 | |||
79 | if (await this.confirmService.confirm(confirmHTML, title) !== true) return | ||
80 | |||
81 | this.isRunningEdition = true | ||
82 | |||
83 | const tasks = this.buildTasks() | ||
84 | |||
85 | this.loadingBar.useRef().start() | ||
86 | |||
87 | return this.videoEditorService.editVideo(this.video.uuid, tasks) | ||
88 | .subscribe({ | ||
89 | next: () => { | ||
90 | this.notifier.success($localize`Video updated.`) | ||
91 | this.router.navigateByUrl(Video.buildWatchUrl(this.video)) | ||
92 | }, | ||
93 | |||
94 | error: err => { | ||
95 | this.loadingBar.useRef().complete() | ||
96 | this.isRunningEdition = false | ||
97 | this.notifier.error(err.message) | ||
98 | console.error(err) | ||
99 | } | ||
100 | }) | ||
101 | } | ||
102 | |||
103 | getIntroOutroTooltip () { | ||
104 | return $localize`(extensions: ${this.videoExtensions.join(', ')})` | ||
105 | } | ||
106 | |||
107 | getWatermarkTooltip () { | ||
108 | return $localize`(extensions: ${this.imageExtensions.join(', ')})` | ||
109 | } | ||
110 | |||
111 | noEdition () { | ||
112 | return this.buildTasks().length === 0 | ||
113 | } | ||
114 | |||
115 | getTasksSummary () { | ||
116 | const tasks = this.buildTasks() | ||
117 | |||
118 | return tasks.map(t => { | ||
119 | if (t.name === 'add-intro') { | ||
120 | return $localize`"${this.getFilename(t.options.file)}" will be added at the beggining of the video` | ||
121 | } | ||
122 | |||
123 | if (t.name === 'add-outro') { | ||
124 | return $localize`"${this.getFilename(t.options.file)}" will be added at the end of the video` | ||
125 | } | ||
126 | |||
127 | if (t.name === 'add-watermark') { | ||
128 | return $localize`"${this.getFilename(t.options.file)}" image watermark will be added to the video` | ||
129 | } | ||
130 | |||
131 | if (t.name === 'cut') { | ||
132 | const { start, end } = t.options | ||
133 | |||
134 | if (start !== undefined && end !== undefined) { | ||
135 | return $localize`Video will begin at ${secondsToTime(start)} and stop at ${secondsToTime(end)}` | ||
136 | } | ||
137 | |||
138 | if (start !== undefined) { | ||
139 | return $localize`Video will begin at ${secondsToTime(start)}` | ||
140 | } | ||
141 | |||
142 | if (end !== undefined) { | ||
143 | return $localize`Video will stop at ${secondsToTime(end)}` | ||
144 | } | ||
145 | } | ||
146 | |||
147 | return '' | ||
148 | }) | ||
149 | } | ||
150 | |||
151 | private getFilename (obj: any) { | ||
152 | return obj.name | ||
153 | } | ||
154 | |||
155 | private buildTasks () { | ||
156 | const tasks: VideoEditorTask[] = [] | ||
157 | const value = this.form.value | ||
158 | |||
159 | const cut = value['cut'] | ||
160 | if (cut['start'] !== 0 || cut['end'] !== this.video.duration) { | ||
161 | |||
162 | const options: VideoEditorTaskCut['options'] = {} | ||
163 | if (cut['start'] !== 0) options.start = cut['start'] | ||
164 | if (cut['end'] !== this.video.duration) options.end = cut['end'] | ||
165 | |||
166 | tasks.push({ | ||
167 | name: 'cut', | ||
168 | options | ||
169 | }) | ||
170 | } | ||
171 | |||
172 | if (value['add-intro']?.['file']) { | ||
173 | tasks.push({ | ||
174 | name: 'add-intro', | ||
175 | options: { | ||
176 | file: value['add-intro']['file'] | ||
177 | } | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | if (value['add-outro']?.['file']) { | ||
182 | tasks.push({ | ||
183 | name: 'add-outro', | ||
184 | options: { | ||
185 | file: value['add-outro']['file'] | ||
186 | } | ||
187 | }) | ||
188 | } | ||
189 | |||
190 | if (value['add-watermark']?.['file']) { | ||
191 | tasks.push({ | ||
192 | name: 'add-watermark', | ||
193 | options: { | ||
194 | file: value['add-watermark']['file'] | ||
195 | } | ||
196 | }) | ||
197 | } | ||
198 | |||
199 | return tasks | ||
200 | } | ||
201 | |||
202 | } | ||
diff --git a/client/src/app/+video-editor/edit/video-editor-edit.resolver.ts b/client/src/app/+video-editor/edit/video-editor-edit.resolver.ts new file mode 100644 index 000000000..7b95ae834 --- /dev/null +++ b/client/src/app/+video-editor/edit/video-editor-edit.resolver.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | |||
2 | import { Injectable } from '@angular/core' | ||
3 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | ||
4 | import { VideoService } from '@app/shared/shared-main' | ||
5 | |||
6 | @Injectable() | ||
7 | export class VideoEditorEditResolver implements Resolve<any> { | ||
8 | constructor ( | ||
9 | private videoService: VideoService | ||
10 | ) { | ||
11 | } | ||
12 | |||
13 | resolve (route: ActivatedRouteSnapshot) { | ||
14 | const videoId: string = route.params['videoId'] | ||
15 | |||
16 | return this.videoService.getVideo({ videoId }) | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/+video-editor/index.ts b/client/src/app/+video-editor/index.ts new file mode 100644 index 000000000..5a9e9fdd0 --- /dev/null +++ b/client/src/app/+video-editor/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-editor.module' | |||
diff --git a/client/src/app/+video-editor/shared/index.ts b/client/src/app/+video-editor/shared/index.ts new file mode 100644 index 000000000..eaf88b6f4 --- /dev/null +++ b/client/src/app/+video-editor/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-editor.service' | |||
diff --git a/client/src/app/+video-editor/shared/video-editor.service.ts b/client/src/app/+video-editor/shared/video-editor.service.ts new file mode 100644 index 000000000..5b7053039 --- /dev/null +++ b/client/src/app/+video-editor/shared/video-editor.service.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { catchError } from 'rxjs' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { objectToFormData } from '@app/helpers' | ||
6 | import { VideoService } from '@app/shared/shared-main' | ||
7 | import { VideoEditorCreateEdition, VideoEditorTask } from '@shared/models' | ||
8 | |||
9 | @Injectable() | ||
10 | export class VideoEditorService { | ||
11 | |||
12 | constructor ( | ||
13 | private authHttp: HttpClient, | ||
14 | private restExtractor: RestExtractor | ||
15 | ) {} | ||
16 | |||
17 | editVideo (videoId: number | string, tasks: VideoEditorTask[]) { | ||
18 | const url = VideoService.BASE_VIDEO_URL + '/' + videoId + '/editor/edit' | ||
19 | const body: VideoEditorCreateEdition = { | ||
20 | tasks | ||
21 | } | ||
22 | |||
23 | const data = objectToFormData(body) | ||
24 | |||
25 | return this.authHttp.post(url, data) | ||
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/+video-editor/video-editor-routing.module.ts b/client/src/app/+video-editor/video-editor-routing.module.ts new file mode 100644 index 000000000..9f37a0dae --- /dev/null +++ b/client/src/app/+video-editor/video-editor-routing.module.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | import { VideoEditorEditResolver } from './edit' | ||
4 | import { VideoEditorEditComponent } from './edit/video-editor-edit.component' | ||
5 | |||
6 | const videoEditorRoutes: Routes = [ | ||
7 | { | ||
8 | path: '', | ||
9 | children: [ | ||
10 | { | ||
11 | path: 'edit/:videoId', | ||
12 | component: VideoEditorEditComponent, | ||
13 | data: { | ||
14 | meta: { | ||
15 | title: $localize`Edit video` | ||
16 | } | ||
17 | }, | ||
18 | resolve: { | ||
19 | video: VideoEditorEditResolver | ||
20 | } | ||
21 | } | ||
22 | ] | ||
23 | } | ||
24 | ] | ||
25 | |||
26 | @NgModule({ | ||
27 | imports: [ RouterModule.forChild(videoEditorRoutes) ], | ||
28 | exports: [ RouterModule ] | ||
29 | }) | ||
30 | export class VideoEditorRoutingModule {} | ||
diff --git a/client/src/app/+video-editor/video-editor.module.ts b/client/src/app/+video-editor/video-editor.module.ts new file mode 100644 index 000000000..7bbebc17b --- /dev/null +++ b/client/src/app/+video-editor/video-editor.module.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedFormModule } from '@app/shared/shared-forms' | ||
3 | import { SharedMainModule } from '@app/shared/shared-main' | ||
4 | import { VideoEditorEditComponent, VideoEditorEditResolver } from './edit' | ||
5 | import { VideoEditorService } from './shared' | ||
6 | import { VideoEditorRoutingModule } from './video-editor-routing.module' | ||
7 | |||
8 | @NgModule({ | ||
9 | imports: [ | ||
10 | VideoEditorRoutingModule, | ||
11 | |||
12 | SharedMainModule, | ||
13 | SharedFormModule | ||
14 | ], | ||
15 | |||
16 | declarations: [ | ||
17 | VideoEditorEditComponent | ||
18 | ], | ||
19 | |||
20 | exports: [], | ||
21 | |||
22 | providers: [ | ||
23 | VideoEditorService, | ||
24 | VideoEditorEditResolver | ||
25 | ] | ||
26 | }) | ||
27 | export class VideoEditorModule { } | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts index 5c4152884..6deadfcbe 100644 --- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts | |||
@@ -77,7 +77,8 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni | |||
77 | 77 | ||
78 | this.captionAdded.emit({ | 78 | this.captionAdded.emit({ |
79 | language: languageObject, | 79 | language: languageObject, |
80 | captionfile: this.form.value['captionfile'] | 80 | captionfile: this.form.value['captionfile'], |
81 | action: 'CREATE' | ||
81 | }) | 82 | }) |
82 | 83 | ||
83 | this.hide() | 84 | this.hide() |
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html new file mode 100644 index 000000000..be6f676c2 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html | |||
@@ -0,0 +1,36 @@ | |||
1 | <ng-template #modal> | ||
2 | <ng-container [formGroup]="form"> | ||
3 | |||
4 | <div class="modal-header"> | ||
5 | <h4 i18n class="modal-title">Edit caption</h4> | ||
6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
7 | </div> | ||
8 | |||
9 | <div class="modal-body"> | ||
10 | <label i18n for="captionFileContent">Caption</label> | ||
11 | <textarea | ||
12 | id="captionFileContent" | ||
13 | formControlName="captionFileContent" | ||
14 | class="form-control caption-textarea" | ||
15 | [ngClass]="{ 'input-error': formErrors['captionFileContent'] }" | ||
16 | > | ||
17 | </textarea> | ||
18 | |||
19 | <div *ngIf="formErrors.captionFileContent" class="form-error"> | ||
20 | {{ formErrors.captionFileContent }} | ||
21 | </div> | ||
22 | </div> | ||
23 | |||
24 | <div class="modal-footer inputs"> | ||
25 | <input | ||
26 | type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" | ||
27 | (click)="cancel()" (key.enter)="cancel()" | ||
28 | > | ||
29 | |||
30 | <input | ||
31 | type="submit" i18n-value value="Edit this caption" class="peertube-button orange-button" | ||
32 | [disabled]="!form.valid" (click)="updateCaption()" | ||
33 | > | ||
34 | </div> | ||
35 | </ng-container> | ||
36 | </ng-template> | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss new file mode 100644 index 000000000..bd96f2b7a --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss | |||
@@ -0,0 +1,4 @@ | |||
1 | .caption-textarea { | ||
2 | min-height: 600px; | ||
3 | } | ||
4 | |||
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts new file mode 100644 index 000000000..f74f3c5ea --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' | ||
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
4 | import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main' | ||
5 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | ||
6 | import { HTMLServerConfig, VideoConstant } from '@shared/models' | ||
7 | import { ServerService } from '../../../../core' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-video-caption-edit-modal', | ||
11 | styleUrls: [ './video-caption-edit-modal.component.scss' ], | ||
12 | templateUrl: './video-caption-edit-modal.component.html' | ||
13 | }) | ||
14 | |||
15 | export class VideoCaptionEditModalComponent extends FormReactive implements OnInit { | ||
16 | @Input() videoCaption: VideoCaptionWithPathEdit | ||
17 | @Input() serverConfig: HTMLServerConfig | ||
18 | |||
19 | @Output() captionEdited = new EventEmitter<VideoCaptionEdit>() | ||
20 | |||
21 | @ViewChild('modal', { static: true }) modal: ElementRef | ||
22 | |||
23 | videoCaptionLanguages: VideoConstant<string>[] = [] | ||
24 | private openedModal: NgbModalRef | ||
25 | |||
26 | constructor ( | ||
27 | protected formValidatorService: FormValidatorService, | ||
28 | private modalService: NgbModal, | ||
29 | private videoCaptionService: VideoCaptionService, | ||
30 | private serverService: ServerService | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.serverService.getVideoLanguages().subscribe(languages => this.videoCaptionLanguages = languages) | ||
37 | |||
38 | this.buildForm({ captionFileContent: VIDEO_CAPTION_FILE_CONTENT_VALIDATOR }) | ||
39 | |||
40 | this.loadCaptionContent() | ||
41 | } | ||
42 | |||
43 | loadCaptionContent () { | ||
44 | const { captionPath } = this.videoCaption | ||
45 | if (!captionPath) return | ||
46 | |||
47 | this.videoCaptionService.getCaptionContent({ captionPath }) | ||
48 | .subscribe(res => { | ||
49 | this.form.patchValue({ | ||
50 | captionFileContent: res | ||
51 | }) | ||
52 | }) | ||
53 | } | ||
54 | |||
55 | show () { | ||
56 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) | ||
57 | } | ||
58 | |||
59 | hide () { | ||
60 | this.openedModal.close() | ||
61 | } | ||
62 | |||
63 | cancel () { | ||
64 | this.hide() | ||
65 | } | ||
66 | |||
67 | updateCaption () { | ||
68 | const format = 'vtt' | ||
69 | const languageId = this.videoCaption.language.id | ||
70 | const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) | ||
71 | |||
72 | this.captionEdited.emit({ | ||
73 | language: languageObject, | ||
74 | captionfile: new File([ this.form.value['captionFileContent'] ], `${languageId}.${format}`, { | ||
75 | type: 'text/vtt', | ||
76 | lastModified: Date.now() | ||
77 | }), | ||
78 | action: 'UPDATE' | ||
79 | }) | ||
80 | |||
81 | this.hide() | ||
82 | } | ||
83 | } | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 9bb13ba88..2281f8631 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -186,6 +186,7 @@ | |||
186 | 186 | ||
187 | <div i18n class="caption-entry-state">Already uploaded ✔</div> | 187 | <div i18n class="caption-entry-state">Already uploaded ✔</div> |
188 | 188 | ||
189 | <span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span> | ||
189 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span> | 190 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span> |
190 | </ng-container> | 191 | </ng-container> |
191 | 192 | ||
@@ -197,6 +198,14 @@ | |||
197 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span> | 198 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span> |
198 | </ng-container> | 199 | </ng-container> |
199 | 200 | ||
201 | <ng-container *ngIf="videoCaption.action === 'UPDATE'"> | ||
202 | <span class="caption-entry-label">{{ videoCaption.language.label }}</span> | ||
203 | |||
204 | <div i18n class="caption-entry-state caption-entry-state-create">Will be edited on update</div> | ||
205 | |||
206 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel edition</span> | ||
207 | </ng-container> | ||
208 | |||
200 | <ng-container *ngIf="videoCaption.action === 'REMOVE'"> | 209 | <ng-container *ngIf="videoCaption.action === 'REMOVE'"> |
201 | <span class="caption-entry-label">{{ videoCaption.language.label }}</span> | 210 | <span class="caption-entry-label">{{ videoCaption.language.label }}</span> |
202 | 211 | ||
@@ -204,6 +213,13 @@ | |||
204 | 213 | ||
205 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span> | 214 | <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span> |
206 | </ng-container> | 215 | </ng-container> |
216 | |||
217 | <my-video-caption-edit-modal | ||
218 | #videoCaptionEditModal | ||
219 | [videoCaption]="videoCaption" | ||
220 | [serverConfig]="serverConfig" | ||
221 | (captionEdited)="onCaptionEdited($event)" | ||
222 | ></my-video-caption-edit-modal> | ||
207 | </div> | 223 | </div> |
208 | </div> | 224 | </div> |
209 | 225 | ||
@@ -373,5 +389,5 @@ | |||
373 | </div> | 389 | </div> |
374 | 390 | ||
375 | <my-video-caption-add-modal | 391 | <my-video-caption-add-modal |
376 | #videoCaptionAddModal [existingCaptions]="getExistingCaptions()" [serverConfig]="serverConfig" (captionAdded)="onCaptionAdded($event)" | 392 | #videoCaptionAddModal [existingCaptions]="getExistingCaptions()" [serverConfig]="serverConfig" (captionAdded)="onCaptionEdited($event)" |
377 | ></my-video-caption-add-modal> | 393 | ></my-video-caption-add-modal> |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss index 4b1dec89a..5344e5431 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss | |||
@@ -96,6 +96,10 @@ my-peertube-checkbox { | |||
96 | } | 96 | } |
97 | } | 97 | } |
98 | 98 | ||
99 | .caption-entry-edit { | ||
100 | @include peertube-button; | ||
101 | } | ||
102 | |||
99 | .caption-entry-delete { | 103 | .caption-entry-delete { |
100 | @include peertube-button; | 104 | @include peertube-button; |
101 | @include grey-button; | 105 | @include grey-button; |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 31dbe43e6..2801fc519 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts | |||
@@ -21,7 +21,7 @@ import { | |||
21 | } from '@app/shared/form-validators/video-validators' | 21 | } from '@app/shared/form-validators/video-validators' |
22 | import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' | 22 | import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' |
23 | import { InstanceService } from '@app/shared/shared-instance' | 23 | import { InstanceService } from '@app/shared/shared-instance' |
24 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' | 24 | import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main' |
25 | import { PluginInfo } from '@root-helpers/plugins-manager' | 25 | import { PluginInfo } from '@root-helpers/plugins-manager' |
26 | import { | 26 | import { |
27 | HTMLServerConfig, | 27 | HTMLServerConfig, |
@@ -34,6 +34,7 @@ import { | |||
34 | } from '@shared/models' | 34 | } from '@shared/models' |
35 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' | 35 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' |
36 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 36 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
37 | import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component' | ||
37 | import { VideoEditType } from './video-edit.type' | 38 | import { VideoEditType } from './video-edit.type' |
38 | 39 | ||
39 | type VideoLanguages = VideoConstant<string> & { group?: string } | 40 | type VideoLanguages = VideoConstant<string> & { group?: string } |
@@ -58,13 +59,14 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
58 | @Input() userVideoChannels: SelectChannelItem[] = [] | 59 | @Input() userVideoChannels: SelectChannelItem[] = [] |
59 | @Input() forbidScheduledPublication = true | 60 | @Input() forbidScheduledPublication = true |
60 | 61 | ||
61 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] | 62 | @Input() videoCaptions: VideoCaptionWithPathEdit[] = [] |
62 | 63 | ||
63 | @Input() waitTranscodingEnabled = true | 64 | @Input() waitTranscodingEnabled = true |
64 | @Input() type: VideoEditType | 65 | @Input() type: VideoEditType |
65 | @Input() liveVideo: LiveVideo | 66 | @Input() liveVideo: LiveVideo |
66 | 67 | ||
67 | @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent | 68 | @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent |
69 | @ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent | ||
68 | 70 | ||
69 | @Output() formBuilt = new EventEmitter<void>() | 71 | @Output() formBuilt = new EventEmitter<void>() |
70 | @Output() pluginFieldsAdded = new EventEmitter<void>() | 72 | @Output() pluginFieldsAdded = new EventEmitter<void>() |
@@ -228,12 +230,12 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
228 | .map(c => c.language.id) | 230 | .map(c => c.language.id) |
229 | } | 231 | } |
230 | 232 | ||
231 | onCaptionAdded (caption: VideoCaptionEdit) { | 233 | onCaptionEdited (caption: VideoCaptionEdit) { |
232 | const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id) | 234 | const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id) |
233 | 235 | ||
234 | // Replace existing caption? | 236 | // Replace existing caption? |
235 | if (existingCaption) { | 237 | if (existingCaption) { |
236 | Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' }) | 238 | Object.assign(existingCaption, caption) |
237 | } else { | 239 | } else { |
238 | this.videoCaptions.push( | 240 | this.videoCaptions.push( |
239 | Object.assign(caption, { action: 'CREATE' as 'CREATE' }) | 241 | Object.assign(caption, { action: 'CREATE' as 'CREATE' }) |
@@ -251,7 +253,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
251 | } | 253 | } |
252 | 254 | ||
253 | // This caption is not on the server, just remove it from our array | 255 | // This caption is not on the server, just remove it from our array |
254 | if (caption.action === 'CREATE') { | 256 | if (caption.action === 'CREATE' || caption.action === 'UPDATE') { |
255 | removeElementFromArray(this.videoCaptions, caption) | 257 | removeElementFromArray(this.videoCaptions, caption) |
256 | return | 258 | return |
257 | } | 259 | } |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts index 7a3854065..4e8767364 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts | |||
@@ -6,6 +6,7 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
6 | import { SharedVideoLiveModule } from '@app/shared/shared-video-live' | 6 | import { SharedVideoLiveModule } from '@app/shared/shared-video-live' |
7 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' | 7 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' |
8 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 8 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
9 | import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component' | ||
9 | import { VideoEditComponent } from './video-edit.component' | 10 | import { VideoEditComponent } from './video-edit.component' |
10 | 11 | ||
11 | @NgModule({ | 12 | @NgModule({ |
@@ -20,7 +21,8 @@ import { VideoEditComponent } from './video-edit.component' | |||
20 | 21 | ||
21 | declarations: [ | 22 | declarations: [ |
22 | VideoEditComponent, | 23 | VideoEditComponent, |
23 | VideoCaptionAddModalComponent | 24 | VideoCaptionAddModalComponent, |
25 | VideoCaptionEditModalComponent | ||
24 | ], | 26 | ], |
25 | 27 | ||
26 | exports: [ | 28 | exports: [ |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts index 3d0e1bf2a..9de373cd3 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts | |||
@@ -2,7 +2,7 @@ import { catchError, switchMap, tap } from 'rxjs/operators' | |||
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 2 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
3 | import { Directive, EventEmitter, OnInit } from '@angular/core' | 3 | import { Directive, EventEmitter, OnInit } from '@angular/core' |
4 | import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' |
5 | import { listUserChannels } from '@app/helpers' | 5 | import { listUserChannelsForSelect } from '@app/helpers' |
6 | import { FormReactive } from '@app/shared/shared-forms' | 6 | import { FormReactive } from '@app/shared/shared-forms' |
7 | import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 8 | import { LoadingBarService } from '@ngx-loading-bar/core' |
@@ -38,7 +38,7 @@ export abstract class VideoSend extends FormReactive implements OnInit { | |||
38 | ngOnInit () { | 38 | ngOnInit () { |
39 | this.buildForm({}) | 39 | this.buildForm({}) |
40 | 40 | ||
41 | listUserChannels(this.authService) | 41 | listUserChannelsForSelect(this.authService) |
42 | .subscribe(channels => { | 42 | .subscribe(channels => { |
43 | this.userVideoChannels = channels | 43 | this.userVideoChannels = channels |
44 | this.firstStepChannelId = this.userVideoChannels[0].id | 44 | this.firstStepChannelId = this.userVideoChannels[0].id |
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 91e76b7fe..82dae5c1c 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators' | |||
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | 4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' |
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { listUserChannels } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' |
8 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
9 | 9 | ||
@@ -33,7 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
33 | .loadCompleteDescription(video.descriptionPath) | 33 | .loadCompleteDescription(video.descriptionPath) |
34 | .pipe(map(description => Object.assign(video, { description }))), | 34 | .pipe(map(description => Object.assign(video, { description }))), |
35 | 35 | ||
36 | listUserChannels(this.authService), | 36 | listUserChannelsForSelect(this.authService), |
37 | 37 | ||
38 | this.videoCaptionService | 38 | this.videoCaptionService |
39 | .listCaptions(video.id) | 39 | .listCaptions(video.id) |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts index e59238ffe..6e8a64f46 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts | |||
@@ -35,6 +35,7 @@ export class ActionButtonsComponent implements OnInit, OnChanges { | |||
35 | playlist: false, | 35 | playlist: false, |
36 | download: true, | 36 | download: true, |
37 | update: true, | 37 | update: true, |
38 | editor: true, | ||
38 | blacklist: true, | 39 | blacklist: true, |
39 | delete: true, | 40 | delete: true, |
40 | report: true, | 41 | report: true, |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html index d0e9bcd29..5014b9692 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }"> | 1 | <div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }"> |
2 | <div class="left"> | 2 | <div class="left"> |
3 | <my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account"></my-actor-avatar> | 3 | <my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account" [size]="isChild() ? '25' : '36'"></my-actor-avatar> |
4 | <div class="vertical-border"></div> | 4 | <div class="vertical-border"></div> |
5 | </div> | 5 | </div> |
6 | 6 | ||
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss index 87e313d41..54f828014 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss | |||
@@ -25,10 +25,6 @@ | |||
25 | } | 25 | } |
26 | } | 26 | } |
27 | 27 | ||
28 | my-actor-avatar { | ||
29 | @include actor-avatar-size(36px); | ||
30 | } | ||
31 | |||
32 | .comment { | 28 | .comment { |
33 | flex-grow: 1; | 29 | flex-grow: 1; |
34 | // Fix word-wrap with flex | 30 | // Fix word-wrap with flex |
@@ -160,11 +156,6 @@ my-video-comment-add { | |||
160 | } | 156 | } |
161 | 157 | ||
162 | .is-child { | 158 | .is-child { |
163 | // Reduce avatars size for replies | ||
164 | my-actor-avatar { | ||
165 | @include actor-avatar-size(25px); | ||
166 | } | ||
167 | |||
168 | .left { | 159 | .left { |
169 | @include margin-right(6px); | 160 | @include margin-right(6px); |
170 | } | 161 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html index 0c4d46714..c6ffb1abd 100644 --- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html +++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html | |||
@@ -14,6 +14,10 @@ | |||
14 | The video is being transcoded, it may not work properly. | 14 | The video is being transcoded, it may not work properly. |
15 | </div> | 15 | </div> |
16 | 16 | ||
17 | <div i18n class="alert alert-warning" *ngIf="isVideoToEdit()"> | ||
18 | The video is being edited, it may not work properly. | ||
19 | </div> | ||
20 | |||
17 | <div i18n class="alert alert-warning" *ngIf="isVideoToMoveToExternalStorage()"> | 21 | <div i18n class="alert alert-warning" *ngIf="isVideoToMoveToExternalStorage()"> |
18 | The video is being moved to an external server, it may not work properly. | 22 | The video is being moved to an external server, it may not work properly. |
19 | </div> | 23 | </div> |
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts index a3d3fa6fb..79b56705f 100644 --- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts +++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts | |||
@@ -14,6 +14,10 @@ export class VideoAlertComponent { | |||
14 | return this.video && this.video.state.id === VideoState.TO_TRANSCODE | 14 | return this.video && this.video.state.id === VideoState.TO_TRANSCODE |
15 | } | 15 | } |
16 | 16 | ||
17 | isVideoToEdit () { | ||
18 | return this.video && this.video.state.id === VideoState.TO_EDIT | ||
19 | } | ||
20 | |||
17 | isVideoTranscodingFailed () { | 21 | isVideoTranscodingFailed () { |
18 | return this.video && this.video.state.id === VideoState.TRANSCODING_FAILED | 22 | return this.video && this.video.state.id === VideoState.TRANSCODING_FAILED |
19 | } | 23 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html index d433c7aba..a23152b67 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html | |||
@@ -2,19 +2,19 @@ | |||
2 | <my-actor-avatar | 2 | <my-actor-avatar |
3 | *ngIf="showChannel" | 3 | *ngIf="showChannel" |
4 | class="channel" | 4 | class="channel" |
5 | [class.main-avatar]="showChannel" | ||
6 | [channel]="video.channel" | 5 | [channel]="video.channel" |
7 | [internalHref]="[ '/c', video.byVideoChannel ]" | 6 | [internalHref]="[ '/c', video.byVideoChannel ]" |
8 | [title]="channelLinkTitle" | 7 | [title]="channelLinkTitle" |
8 | size="35" | ||
9 | ></my-actor-avatar> | 9 | ></my-actor-avatar> |
10 | 10 | ||
11 | <my-actor-avatar | 11 | <my-actor-avatar |
12 | *ngIf="showAccount" | 12 | *ngIf="showAccount" |
13 | class="account" | 13 | class="account" |
14 | [class.main-avatar]="!showChannel" | ||
15 | [class.second-avatar]="showChannel" | 14 | [class.second-avatar]="showChannel" |
16 | [account]="video.account" | 15 | [account]="video.account" |
17 | [internalHref]="[ '/a', video.byAccount ]" | 16 | [internalHref]="[ '/a', video.byAccount ]" |
18 | [title]="accountLinkTitle"> | 17 | [title]="accountLinkTitle" |
18 | size="35"> | ||
19 | </my-actor-avatar> | 19 | </my-actor-avatar> |
20 | </div> | 20 | </div> |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss index 71c5e4b5a..80711ff32 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss | |||
@@ -1,9 +1,5 @@ | |||
1 | @use '_mixins' as *; | 1 | @use '_mixins' as *; |
2 | 2 | ||
3 | @mixin main { | ||
4 | @include actor-avatar-size(35px); | ||
5 | } | ||
6 | |||
7 | @mixin secondary { | 3 | @mixin secondary { |
8 | height: 60%; | 4 | height: 60%; |
9 | width: 60%; | 5 | width: 60%; |
@@ -14,16 +10,11 @@ | |||
14 | } | 10 | } |
15 | 11 | ||
16 | .wrapper { | 12 | .wrapper { |
17 | @include actor-avatar-size(35px); | ||
18 | @include margin-right(5px); | 13 | @include margin-right(5px); |
19 | 14 | ||
20 | position: relative; | 15 | position: relative; |
21 | margin-bottom: 5px; | 16 | margin-bottom: 5px; |
22 | 17 | ||
23 | .main-avatar { | ||
24 | @include main(); | ||
25 | } | ||
26 | |||
27 | .second-avatar { | 18 | .second-avatar { |
28 | @include secondary(); | 19 | @include secondary(); |
29 | } | 20 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts index 146c440b3..4bfc7bd9d 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts | |||
@@ -20,8 +20,4 @@ export class VideoAvatarChannelComponent implements OnInit { | |||
20 | this.channelLinkTitle = $localize`${this.video.account.name} (channel page)` | 20 | this.channelLinkTitle = $localize`${this.video.account.name} (channel page)` |
21 | this.accountLinkTitle = $localize`${this.video.byAccount} (account page)` | 21 | this.accountLinkTitle = $localize`${this.video.byAccount} (account page)` |
22 | } | 22 | } |
23 | |||
24 | isChannelAvatarNull () { | ||
25 | return this.video.channel.avatar === null | ||
26 | } | ||
27 | } | 23 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss index 75ed9d901..fc67ac65a 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss +++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss | |||
@@ -4,6 +4,7 @@ | |||
4 | @use '_miniature' as *; | 4 | @use '_miniature' as *; |
5 | 5 | ||
6 | .playlist { | 6 | .playlist { |
7 | position: relative; | ||
7 | min-width: 200px; | 8 | min-width: 200px; |
8 | max-width: 470px; | 9 | max-width: 470px; |
9 | height: 66vh; | 10 | height: 66vh; |
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts index b44238310..879d296a7 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, Output } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService, ComponentPagination, HooksService, Notifier, SessionStorageService, UserService } from '@app/core' | 3 | import { AuthService, ComponentPagination, HooksService, Notifier, SessionStorageService, UserService } from '@app/core' |
4 | import { isInViewport } from '@app/helpers' | ||
4 | import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 5 | import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
5 | import { peertubeSessionStorage } from '@root-helpers/peertube-web-storage' | ||
6 | import { getBoolOrDefault } from '@root-helpers/local-storage-utils' | 6 | import { getBoolOrDefault } from '@root-helpers/local-storage-utils' |
7 | import { peertubeSessionStorage } from '@root-helpers/peertube-web-storage' | ||
7 | import { VideoPlaylistPrivacy } from '@shared/models' | 8 | import { VideoPlaylistPrivacy } from '@shared/models' |
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
@@ -132,7 +133,12 @@ export class VideoWatchPlaylistComponent { | |||
132 | this.videoFound.emit(playlistElement.video.uuid) | 133 | this.videoFound.emit(playlistElement.video.uuid) |
133 | 134 | ||
134 | setTimeout(() => { | 135 | setTimeout(() => { |
135 | document.querySelector('.element-' + this.currentPlaylistPosition).scrollIntoView(false) | 136 | const element = document.querySelector<HTMLElement>('.element-' + this.currentPlaylistPosition) |
137 | const container = document.querySelector<HTMLElement>('.playlist') | ||
138 | |||
139 | if (isInViewport(element, container)) return | ||
140 | |||
141 | container.scrollTop = element.offsetTop | ||
136 | }) | 142 | }) |
137 | 143 | ||
138 | return | 144 | return |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index 830215225..4c15ae3d7 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -11,11 +11,7 @@ | |||
11 | <img class="placeholder-image" *ngIf="playerPlaceholderImgSrc" [src]="playerPlaceholderImgSrc" alt="Placeholder image" i18n-alt> | 11 | <img class="placeholder-image" *ngIf="playerPlaceholderImgSrc" [src]="playerPlaceholderImgSrc" alt="Placeholder image" i18n-alt> |
12 | </div> | 12 | </div> |
13 | 13 | ||
14 | <my-video-watch-playlist | 14 | <my-video-watch-playlist #videoWatchPlaylist [playlist]="playlist" (videoFound)="onPlaylistVideoFound($event)"></my-video-watch-playlist> |
15 | #videoWatchPlaylist | ||
16 | [playlist]="playlist" class="playlist" | ||
17 | (videoFound)="onPlaylistVideoFound($event)" | ||
18 | ></my-video-watch-playlist> | ||
19 | 15 | ||
20 | <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder> | 16 | <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder> |
21 | </div> | 17 | </div> |
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html index 1a715560c..f250c2407 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.html +++ b/client/src/app/+videos/video-list/overview/video-overview.component.html | |||
@@ -33,7 +33,7 @@ | |||
33 | <div class="section channel videos" *ngFor="let object of overview.channels"> | 33 | <div class="section channel videos" *ngFor="let object of overview.channels"> |
34 | <div class="section-title"> | 34 | <div class="section-title"> |
35 | <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]"> | 35 | <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]"> |
36 | <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar> | 36 | <my-actor-avatar [channel]="buildVideoChannel(object)" size="28"></my-actor-avatar> |
37 | 37 | ||
38 | <h2 class="section-title">{{ object.channel.displayName }}</h2> | 38 | <h2 class="section-title">{{ object.channel.displayName }}</h2> |
39 | </a> | 39 | </a> |
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.scss b/client/src/app/+videos/video-list/overview/video-overview.component.scss index 2239d1913..8b2aa88f2 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.scss +++ b/client/src/app/+videos/video-list/overview/video-overview.component.scss | |||
@@ -52,7 +52,6 @@ | |||
52 | align-items: center; | 52 | align-items: center; |
53 | 53 | ||
54 | my-actor-avatar { | 54 | my-actor-avatar { |
55 | @include actor-avatar-size(28px); | ||
56 | @include margin-right(8px); | 55 | @include margin-right(8px); |
57 | 56 | ||
58 | font-size: initial; | 57 | font-size: initial; |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index b5afc9c92..cd499845b 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -143,6 +143,12 @@ const routes: Routes = [ | |||
143 | canActivateChild: [ MetaGuard ] | 143 | canActivateChild: [ MetaGuard ] |
144 | }, | 144 | }, |
145 | 145 | ||
146 | { | ||
147 | path: 'video-editor', | ||
148 | loadChildren: () => import('./+video-editor/video-editor.module').then(m => m.VideoEditorModule), | ||
149 | canActivateChild: [ MetaGuard ] | ||
150 | }, | ||
151 | |||
146 | // Matches /@:actorName | 152 | // Matches /@:actorName |
147 | { | 153 | { |
148 | matcher: (url): UrlMatchResult => { | 154 | matcher: (url): UrlMatchResult => { |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index f211051ce..6ba30e4b8 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -132,8 +132,8 @@ export class User implements UserServerModel { | |||
132 | } | 132 | } |
133 | } | 133 | } |
134 | 134 | ||
135 | updateAccountAvatar (newAccountAvatar?: ActorImage) { | 135 | updateAccountAvatar (newAccountAvatars?: ActorImage[]) { |
136 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) | 136 | if (newAccountAvatars) this.account.updateAvatar(newAccountAvatars) |
137 | else this.account.resetAvatar() | 137 | else this.account.resetAvatar() |
138 | } | 138 | } |
139 | 139 | ||
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index b14bff193..b4024c02d 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -118,7 +118,7 @@ export class UserService { | |||
118 | changeAvatar (avatarForm: FormData) { | 118 | changeAvatar (avatarForm: FormData) { |
119 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' | 119 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' |
120 | 120 | ||
121 | return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm) | 121 | return this.authHttp.post<{ avatars: ActorImage[] }>(url, avatarForm) |
122 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 122 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
123 | } | 123 | } |
124 | 124 | ||
diff --git a/client/src/app/helpers/utils/channel.ts b/client/src/app/helpers/utils/channel.ts index 93863a8af..83f36b70f 100644 --- a/client/src/app/helpers/utils/channel.ts +++ b/client/src/app/helpers/utils/channel.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import { minBy } from 'lodash-es' | ||
1 | import { first, map } from 'rxjs/operators' | 2 | import { first, map } from 'rxjs/operators' |
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 3 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
4 | import { VideoChannel } from '@shared/models' | ||
3 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
4 | 6 | ||
5 | function listUserChannels (authService: AuthService) { | 7 | function listUserChannelsForSelect (authService: AuthService) { |
6 | return authService.userInformationLoaded | 8 | return authService.userInformationLoaded |
7 | .pipe( | 9 | .pipe( |
8 | first(), | 10 | first(), |
@@ -23,12 +25,20 @@ function listUserChannels (authService: AuthService) { | |||
23 | id: c.id, | 25 | id: c.id, |
24 | label: c.displayName, | 26 | label: c.displayName, |
25 | support: c.support, | 27 | support: c.support, |
26 | avatarPath: c.avatar?.path | 28 | avatarPath: getAvatarPath(c) |
27 | }) as SelectChannelItem) | 29 | }) as SelectChannelItem) |
28 | }) | 30 | }) |
29 | ) | 31 | ) |
30 | } | 32 | } |
31 | 33 | ||
32 | export { | 34 | export { |
33 | listUserChannels | 35 | listUserChannelsForSelect |
36 | } | ||
37 | |||
38 | // --------------------------------------------------------------------------- | ||
39 | |||
40 | function getAvatarPath (c: VideoChannel) { | ||
41 | if (!c.avatars || c.avatars.length === 0) return undefined | ||
42 | |||
43 | return minBy(c.avatars, 'width').path | ||
34 | } | 44 | } |
diff --git a/client/src/app/helpers/utils/ui.ts b/client/src/app/helpers/utils/dom.ts index ac8298926..f65e4d726 100644 --- a/client/src/app/helpers/utils/ui.ts +++ b/client/src/app/helpers/utils/dom.ts | |||
@@ -6,14 +6,24 @@ function scrollToTop (behavior: 'auto' | 'smooth' = 'auto') { | |||
6 | }) | 6 | }) |
7 | } | 7 | } |
8 | 8 | ||
9 | function isInViewport (el: HTMLElement) { | 9 | function isInViewport (el: HTMLElement, container: HTMLElement = document.documentElement) { |
10 | const bounding = el.getBoundingClientRect() | 10 | const boundingEl = el.getBoundingClientRect() |
11 | return ( | 11 | const boundingContainer = container.getBoundingClientRect() |
12 | bounding.top >= 0 && | 12 | |
13 | bounding.left >= 0 && | 13 | const relativePos = { |
14 | bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && | 14 | top: 0, |
15 | bounding.right <= (window.innerWidth || document.documentElement.clientWidth) | 15 | left: 0, |
16 | ) | 16 | bottom: 0, |
17 | right: 0 | ||
18 | } | ||
19 | |||
20 | relativePos.top = boundingEl.top - boundingContainer.top | ||
21 | relativePos.left = boundingEl.left - boundingContainer.left | ||
22 | |||
23 | return relativePos.top >= 0 && | ||
24 | relativePos.left >= 0 && | ||
25 | boundingEl.bottom <= boundingContainer.bottom && | ||
26 | boundingEl.right <= boundingContainer.right | ||
17 | } | 27 | } |
18 | 28 | ||
19 | function isXPercentInViewport (el: HTMLElement, percentVisible: number) { | 29 | function isXPercentInViewport (el: HTMLElement, percentVisible: number) { |
diff --git a/client/src/app/helpers/utils/index.ts b/client/src/app/helpers/utils/index.ts index dc09c92ab..f821985c9 100644 --- a/client/src/app/helpers/utils/index.ts +++ b/client/src/app/helpers/utils/index.ts | |||
@@ -2,6 +2,6 @@ export * from './channel' | |||
2 | export * from './date' | 2 | export * from './date' |
3 | export * from './html' | 3 | export * from './html' |
4 | export * from './object' | 4 | export * from './object' |
5 | export * from './ui' | 5 | export * from './dom' |
6 | export * from './upload' | 6 | export * from './upload' |
7 | export * from './url' | 7 | export * from './url' |
diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts index b3cded8f4..08c27e3c1 100644 --- a/client/src/app/helpers/utils/url.ts +++ b/client/src/app/helpers/utils/url.ts | |||
@@ -13,6 +13,10 @@ function getAbsoluteAPIUrl () { | |||
13 | return absoluteAPIUrl | 13 | return absoluteAPIUrl |
14 | } | 14 | } |
15 | 15 | ||
16 | function getAPIHost () { | ||
17 | return new URL(getAbsoluteAPIUrl()).host | ||
18 | } | ||
19 | |||
16 | function getAbsoluteEmbedUrl () { | 20 | function getAbsoluteEmbedUrl () { |
17 | let absoluteEmbedUrl = environment.originServerUrl | 21 | let absoluteEmbedUrl = environment.originServerUrl |
18 | if (!absoluteEmbedUrl) { | 22 | if (!absoluteEmbedUrl) { |
@@ -52,5 +56,6 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { | |||
52 | export { | 56 | export { |
53 | objectToFormData, | 57 | objectToFormData, |
54 | getAbsoluteAPIUrl, | 58 | getAbsoluteAPIUrl, |
59 | getAPIHost, | ||
55 | getAbsoluteEmbedUrl | 60 | getAbsoluteEmbedUrl |
56 | } | 61 | } |
diff --git a/client/src/app/modal/account-setup-warning-modal.component.ts b/client/src/app/modal/account-setup-warning-modal.component.ts index c78de447d..c4cbf92b6 100644 --- a/client/src/app/modal/account-setup-warning-modal.component.ts +++ b/client/src/app/modal/account-setup-warning-modal.component.ts | |||
@@ -32,7 +32,7 @@ export class AccountSetupWarningModalComponent { | |||
32 | } | 32 | } |
33 | 33 | ||
34 | hasAccountAvatar (user: User) { | 34 | hasAccountAvatar (user: User) { |
35 | return !!user.account.avatar | 35 | return user.account.avatars.length !== 0 |
36 | } | 36 | } |
37 | 37 | ||
38 | hasAccountDescription (user: User) { | 38 | hasAccountDescription (user: User) { |
diff --git a/client/src/app/shared/form-validators/video-captions-validators.ts b/client/src/app/shared/form-validators/video-captions-validators.ts index a16216422..e589fe934 100644 --- a/client/src/app/shared/form-validators/video-captions-validators.ts +++ b/client/src/app/shared/form-validators/video-captions-validators.ts | |||
@@ -14,3 +14,10 @@ export const VIDEO_CAPTION_FILE_VALIDATOR: BuildFormValidator = { | |||
14 | required: $localize`Video caption file is required.` | 14 | required: $localize`Video caption file is required.` |
15 | } | 15 | } |
16 | } | 16 | } |
17 | |||
18 | export const VIDEO_CAPTION_FILE_CONTENT_VALIDATOR: BuildFormValidator = { | ||
19 | VALIDATORS: [ Validators.required ], | ||
20 | MESSAGES: { | ||
21 | required: $localize`Caption content is required.` | ||
22 | } | ||
23 | } | ||
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html index 07cc73461..f0a27c6e2 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html | |||
@@ -43,7 +43,7 @@ | |||
43 | <td *ngIf="isAdminView()"> | 43 | <td *ngIf="isAdminView()"> |
44 | <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 44 | <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
45 | <div class="chip two-lines"> | 45 | <div class="chip two-lines"> |
46 | <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar> | 46 | <my-actor-avatar [account]="abuse.reporterAccount" size="32"></my-actor-avatar> |
47 | <div> | 47 | <div> |
48 | {{ abuse.reporterAccount.displayName }} | 48 | {{ abuse.reporterAccount.displayName }} |
49 | <span>{{ abuse.reporterAccount.nameWithHost }}</span> | 49 | <span>{{ abuse.reporterAccount.nameWithHost }}</span> |
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts index 8b7d64ed3..01bb401fb 100644 --- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts +++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts | |||
@@ -72,7 +72,7 @@ export class ActorAvatarEditComponent implements OnInit { | |||
72 | } | 72 | } |
73 | 73 | ||
74 | hasAvatar () { | 74 | hasAvatar () { |
75 | return !!this.preview || !!this.actor.avatar | 75 | return !!this.preview || this.actor.avatars.length !== 0 |
76 | } | 76 | } |
77 | 77 | ||
78 | isChannel () { | 78 | isChannel () { |
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.html b/client/src/app/shared/shared-actor-image/actor-avatar.component.html index 13a5385a8..c285b6cc3 100644 --- a/client/src/app/shared/shared-actor-image/actor-avatar.component.html +++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #img> | 1 | <ng-template #img> |
2 | <img *ngIf="previewImage || avatarUrl || !initial" [class]="getClass('avatar')" [src]="previewImage || avatarUrl || defaultAvatarUrl" [alt]="alt" /> | 2 | <img *ngIf="previewImage || avatarUrl || !initial" [class]="getClass('avatar')" [src]="previewImage || avatarUrl || defaultAvatarUrl" [alt]="alt" /> |
3 | 3 | ||
4 | <div *ngIf="!avatarUrl && initial" [class]="getClass('initial')"> | 4 | <div *ngIf="!avatarUrl && initial" [ngClass]="getClass('initial')"> |
5 | <span>{{ initial }}</span> | 5 | <span>{{ initial }}</span> |
6 | </div> | 6 | </div> |
7 | </ng-template> | 7 | </ng-template> |
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar.component.scss index a2424b593..68bf74553 100644 --- a/client/src/app/shared/shared-actor-image/actor-avatar.component.scss +++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.scss | |||
@@ -20,38 +20,23 @@ | |||
20 | } | 20 | } |
21 | } | 21 | } |
22 | 22 | ||
23 | .avatar-18 { | 23 | $sizes: '18', '25', '28', '32', '34', '35', '36', '40', '48', '75', '80', '100', '120'; |
24 | --avatarSize: 18px; | ||
25 | --initialFontSize: 13px; | ||
26 | } | ||
27 | 24 | ||
28 | .avatar-25 { | 25 | @each $size in $sizes { |
29 | --avatarSize: 25px; | 26 | .avatar-#{$size} { |
27 | --avatarSize: #{$size}px; | ||
28 | } | ||
30 | } | 29 | } |
31 | 30 | ||
32 | .avatar-32 { | 31 | .avatar-18 { |
33 | --avatarSize: 32px; | 32 | --initialFontSize: 13px; |
34 | } | ||
35 | |||
36 | .avatar-34 { | ||
37 | --avatarSize: 34px; | ||
38 | } | ||
39 | |||
40 | .avatar-36 { | ||
41 | --avatarSize: 36px; | ||
42 | } | ||
43 | |||
44 | .avatar-40 { | ||
45 | --avatarSize: 40px; | ||
46 | } | 33 | } |
47 | 34 | ||
48 | .avatar-100 { | 35 | .avatar-100 { |
49 | --avatarSize: 100px; | ||
50 | --initialFontSize: 40px; | 36 | --initialFontSize: 40px; |
51 | } | 37 | } |
52 | 38 | ||
53 | .avatar-120 { | 39 | .avatar-120 { |
54 | --avatarSize: 120px; | ||
55 | --initialFontSize: 46px; | 40 | --initialFontSize: 46px; |
56 | } | 41 | } |
57 | 42 | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts index c323dc724..b2e1ef46e 100644 --- a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts +++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts | |||
@@ -4,11 +4,11 @@ import { Account } from '../shared-main/account/account.model' | |||
4 | 4 | ||
5 | type ActorInput = { | 5 | type ActorInput = { |
6 | name: string | 6 | name: string |
7 | avatar?: { url?: string, path: string } | 7 | avatars: { width: number, url?: string, path: string }[] |
8 | url: string | 8 | url: string |
9 | } | 9 | } |
10 | 10 | ||
11 | export type ActorAvatarSize = '18' | '25' | '32' | '34' | '36' | '40' | '100' | '120' | 11 | export type ActorAvatarSize = '18' | '25' | '28' | '32' | '34' | '35' | '36' | '40' | '48' | '75' | '80' | '100' | '120' |
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-actor-avatar', | 14 | selector: 'my-actor-avatar', |
@@ -23,7 +23,7 @@ export class ActorAvatarComponent { | |||
23 | 23 | ||
24 | @Input() previewImage: string | 24 | @Input() previewImage: string |
25 | 25 | ||
26 | @Input() size: ActorAvatarSize | 26 | @Input() size: ActorAvatarSize = '32' |
27 | 27 | ||
28 | // Use an external link | 28 | // Use an external link |
29 | @Input() href: string | 29 | @Input() href: string |
@@ -50,14 +50,13 @@ export class ActorAvatarComponent { | |||
50 | } | 50 | } |
51 | 51 | ||
52 | get defaultAvatarUrl () { | 52 | get defaultAvatarUrl () { |
53 | if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL() | 53 | if (this.account) return Account.GET_DEFAULT_AVATAR_URL(+this.size) |
54 | 54 | if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL(+this.size) | |
55 | return Account.GET_DEFAULT_AVATAR_URL() | ||
56 | } | 55 | } |
57 | 56 | ||
58 | get avatarUrl () { | 57 | get avatarUrl () { |
59 | if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account) | 58 | if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account, +this.size) |
60 | if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel) | 59 | if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel, +this.size) |
61 | 60 | ||
62 | return '' | 61 | return '' |
63 | } | 62 | } |
@@ -90,9 +89,11 @@ export class ActorAvatarComponent { | |||
90 | } | 89 | } |
91 | 90 | ||
92 | private getColorTheme () { | 91 | private getColorTheme () { |
92 | const initialLowercase = this.initial.toLowerCase() | ||
93 | |||
93 | // Keep consistency with CSS | 94 | // Keep consistency with CSS |
94 | const themes = { | 95 | const themes = { |
95 | abc: 'blue', | 96 | '0123456789abc': 'blue', |
96 | def: 'green', | 97 | def: 'green', |
97 | ghi: 'purple', | 98 | ghi: 'purple', |
98 | jkl: 'gray', | 99 | jkl: 'gray', |
@@ -103,7 +104,7 @@ export class ActorAvatarComponent { | |||
103 | } | 104 | } |
104 | 105 | ||
105 | const theme = Object.keys(themes) | 106 | const theme = Object.keys(themes) |
106 | .find(chars => chars.includes(this.initial)) | 107 | .find(chars => chars.includes(initialLowercase)) |
107 | 108 | ||
108 | return themes[theme] | 109 | return themes[theme] |
109 | } | 110 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html index de150aac9..52a402329 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <div *ngIf="channel" class="channel"> | 1 | <div *ngIf="channel" class="channel"> |
2 | 2 | ||
3 | <div class="channel-avatar-row"> | 3 | <div class="channel-avatar-row"> |
4 | <my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel"></my-actor-avatar> | 4 | <my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel" size="75"></my-actor-avatar> |
5 | 5 | ||
6 | <h6> | 6 | <h6> |
7 | <a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel"> | 7 | <a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel"> |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss index 39e8d2091..e8ef478d9 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss | |||
@@ -26,8 +26,6 @@ | |||
26 | } | 26 | } |
27 | 27 | ||
28 | my-actor-avatar { | 28 | my-actor-avatar { |
29 | @include actor-avatar-size(75px); | ||
30 | |||
31 | grid-column: 1; | 29 | grid-column: 1; |
32 | grid-row: 1 / 4; | 30 | grid-row: 1 / 4; |
33 | } | 31 | } |
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts index 07a12c6f6..6b3a6c773 100644 --- a/client/src/app/shared/shared-forms/form-reactive.ts +++ b/client/src/app/shared/shared-forms/form-reactive.ts | |||
@@ -24,7 +24,7 @@ export abstract class FormReactive { | |||
24 | this.formErrors = formErrors | 24 | this.formErrors = formErrors |
25 | this.validationMessages = validationMessages | 25 | this.validationMessages = validationMessages |
26 | 26 | ||
27 | this.form.statusChanges.subscribe(async status => { | 27 | this.form.statusChanges.subscribe(async () => { |
28 | // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed | 28 | // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed |
29 | await this.waitPendingCheck() | 29 | await this.waitPendingCheck() |
30 | 30 | ||
diff --git a/client/src/app/shared/shared-forms/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts index 0fe50ac9b..f67d5bb33 100644 --- a/client/src/app/shared/shared-forms/form-validator.service.ts +++ b/client/src/app/shared/shared-forms/form-validator.service.ts | |||
@@ -30,7 +30,7 @@ export class FormValidatorService { | |||
30 | 30 | ||
31 | if (field?.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string } | 31 | if (field?.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string } |
32 | 32 | ||
33 | const defaultValue = defaultValues[name] || '' | 33 | const defaultValue = defaultValues[name] ?? '' |
34 | 34 | ||
35 | if (field?.VALIDATORS) group[name] = [ defaultValue, field.VALIDATORS ] | 35 | if (field?.VALIDATORS) group[name] = [ defaultValue, field.VALIDATORS ] |
36 | else group[name] = [ defaultValue ] | 36 | else group[name] = [ defaultValue ] |
diff --git a/client/src/app/shared/shared-forms/select/select-channel.component.ts b/client/src/app/shared/shared-forms/select/select-channel.component.ts index 40a7c53bb..5fcae0050 100644 --- a/client/src/app/shared/shared-forms/select/select-channel.component.ts +++ b/client/src/app/shared/shared-forms/select/select-channel.component.ts | |||
@@ -31,7 +31,7 @@ export class SelectChannelComponent implements ControlValueAccessor, OnChanges { | |||
31 | this.channels = this.items.map(c => { | 31 | this.channels = this.items.map(c => { |
32 | const avatarPath = c.avatarPath | 32 | const avatarPath = c.avatarPath |
33 | ? c.avatarPath | 33 | ? c.avatarPath |
34 | : VideoChannel.GET_DEFAULT_AVATAR_URL() | 34 | : VideoChannel.GET_DEFAULT_AVATAR_URL(20) |
35 | 35 | ||
36 | return Object.assign({}, c, { avatarPath }) | 36 | return Object.assign({}, c, { avatarPath }) |
37 | }) | 37 | }) |
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.html b/client/src/app/shared/shared-forms/timestamp-input.component.html index c57a4b32c..c89a7b019 100644 --- a/client/src/app/shared/shared-forms/timestamp-input.component.html +++ b/client/src/app/shared/shared-forms/timestamp-input.component.html | |||
@@ -1,4 +1,5 @@ | |||
1 | <p-inputMask | 1 | <p-inputMask |
2 | [disabled]="disabled" [(ngModel)]="timestampString" (onBlur)="onBlur()" | 2 | [disabled]="disabled" [(ngModel)]="timestampString" (onBlur)="onBlur()" |
3 | mask="9:99:99" slotChar="0" (ngModelChange)="onModelChange()" | 3 | [ngClass]="{ 'border-disabled': disableBorder }" |
4 | mask="9:99:99" slotChar="0" (ngModelChange)="onModelChange()" [inputId]="inputName" | ||
4 | ></p-inputMask> | 5 | ></p-inputMask> |
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.scss b/client/src/app/shared/shared-forms/timestamp-input.component.scss index d2358c027..27d6fa173 100644 --- a/client/src/app/shared/shared-forms/timestamp-input.component.scss +++ b/client/src/app/shared/shared-forms/timestamp-input.component.scss | |||
@@ -1,10 +1,10 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | ||
2 | 3 | ||
3 | p-inputmask { | 4 | p-inputmask { |
4 | ::ng-deep input { | 5 | ::ng-deep input { |
5 | width: 80px; | 6 | width: 80px; |
6 | font-size: 15px; | 7 | font-size: 15px; |
7 | border: 0; | ||
8 | 8 | ||
9 | &:focus-within, | 9 | &:focus-within, |
10 | &:focus { | 10 | &:focus { |
@@ -16,4 +16,16 @@ p-inputmask { | |||
16 | opacity: 0.5; | 16 | opacity: 0.5; |
17 | } | 17 | } |
18 | } | 18 | } |
19 | |||
20 | &.border-disabled { | ||
21 | ::ng-deep input { | ||
22 | border: 0; | ||
23 | } | ||
24 | } | ||
25 | |||
26 | &:not(.border-disabled) { | ||
27 | ::ng-deep input { | ||
28 | @include peertube-input-text(80px); | ||
29 | } | ||
30 | } | ||
19 | } | 31 | } |
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.ts b/client/src/app/shared/shared-forms/timestamp-input.component.ts index 3fc705905..79ca63673 100644 --- a/client/src/app/shared/shared-forms/timestamp-input.component.ts +++ b/client/src/app/shared/shared-forms/timestamp-input.component.ts | |||
@@ -18,6 +18,8 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit { | |||
18 | @Input() maxTimestamp: number | 18 | @Input() maxTimestamp: number |
19 | @Input() timestamp: number | 19 | @Input() timestamp: number |
20 | @Input() disabled = false | 20 | @Input() disabled = false |
21 | @Input() inputName: string | ||
22 | @Input() disableBorder = true | ||
21 | 23 | ||
22 | @Output() inputBlur = new EventEmitter() | 24 | @Output() inputBlur = new EventEmitter() |
23 | 25 | ||
diff --git a/client/src/app/shared/shared-instance/instance-statistics.component.html b/client/src/app/shared/shared-instance/instance-statistics.component.html index 399cf10fe..2ca61fd94 100644 --- a/client/src/app/shared/shared-instance/instance-statistics.component.html +++ b/client/src/app/shared/shared-instance/instance-statistics.component.html | |||
@@ -1,13 +1,13 @@ | |||
1 | <p i18n *ngIf="null === serverStats">Loading instance statistics...</p> | 1 | <p i18n *ngIf="null === serverStats">Loading instance statistics...</p> |
2 | 2 | ||
3 | <section *ngIf="null !== serverStats"> | 3 | <section *ngIf="null !== serverStats"> |
4 | <h3 i18n>Local</h3> | 4 | <h3 i18n>By users on this instance</h3> |
5 | 5 | ||
6 | <div class="row"> | 6 | <div class="row"> |
7 | <div class="col-6 col-lg-4 col-xl-3"> | 7 | <div class="col-6 col-lg-4 col-xl-3"> |
8 | <div class="card stat"> | 8 | <div class="card stat"> |
9 | <div class="card-body"> | 9 | <div class="card-body"> |
10 | <p class="stat-value">{{ serverStats.totalUsers }}</p> | 10 | <p class="stat-value">{{ serverStats.totalUsers | number }}</p> |
11 | <p class="stat-label" i18n>users</p> | 11 | <p class="stat-label" i18n>users</p> |
12 | </div> | 12 | </div> |
13 | <i class="glyphicon glyphicon-user icon-bottom"></i> | 13 | <i class="glyphicon glyphicon-user icon-bottom"></i> |
@@ -17,7 +17,7 @@ | |||
17 | <div class="col-6 col-lg-4 col-xl-3"> | 17 | <div class="col-6 col-lg-4 col-xl-3"> |
18 | <div class="card stat"> | 18 | <div class="card stat"> |
19 | <div class="card-body"> | 19 | <div class="card-body"> |
20 | <p class="stat-value">{{ serverStats.totalLocalVideos }}</p> | 20 | <p class="stat-value">{{ serverStats.totalLocalVideos | number }}</p> |
21 | <p class="stat-label" i18n>videos</p> | 21 | <p class="stat-label" i18n>videos</p> |
22 | </div> | 22 | </div> |
23 | <i class="glyphicon glyphicon-facetime-video"></i> | 23 | <i class="glyphicon glyphicon-facetime-video"></i> |
@@ -27,8 +27,8 @@ | |||
27 | <div class="col-6 col-lg-4 col-xl-3"> | 27 | <div class="col-6 col-lg-4 col-xl-3"> |
28 | <div class="card stat"> | 28 | <div class="card stat"> |
29 | <div class="card-body"> | 29 | <div class="card-body"> |
30 | <p class="stat-value">{{ serverStats.totalLocalVideoViews }}</p> | 30 | <p class="stat-value">{{ serverStats.totalLocalVideoViews | number }}</p> |
31 | <p class="stat-label" i18n>video views</p> | 31 | <p class="stat-label" i18n>views</p> |
32 | </div> | 32 | </div> |
33 | <i class="glyphicon glyphicon-eye-open"></i> | 33 | <i class="glyphicon glyphicon-eye-open"></i> |
34 | </div> | 34 | </div> |
@@ -37,8 +37,8 @@ | |||
37 | <div class="col-6 col-lg-4 col-xl-3"> | 37 | <div class="col-6 col-lg-4 col-xl-3"> |
38 | <div class="card stat"> | 38 | <div class="card stat"> |
39 | <div class="card-body"> | 39 | <div class="card-body"> |
40 | <p class="stat-value">{{ serverStats.totalLocalVideoComments }}</p> | 40 | <p class="stat-value">{{ serverStats.totalLocalVideoComments | number }}</p> |
41 | <p class="stat-label" i18n>video comments</p> | 41 | <p class="stat-label" i18n>comments</p> |
42 | </div> | 42 | </div> |
43 | <i class="glyphicon glyphicon-comment"></i> | 43 | <i class="glyphicon glyphicon-comment"></i> |
44 | </div> | 44 | </div> |
@@ -48,20 +48,20 @@ | |||
48 | <div class="card stat"> | 48 | <div class="card stat"> |
49 | <div class="card-body"> | 49 | <div class="card-body"> |
50 | <p class="stat-value">{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}</p> | 50 | <p class="stat-value">{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}</p> |
51 | <p class="stat-label" i18n>of hosted video</p> | 51 | <p class="stat-label" i18n>hosted video</p> |
52 | </div> | 52 | </div> |
53 | <i class="glyphicon glyphicon-hdd"></i> | 53 | <i class="glyphicon glyphicon-hdd"></i> |
54 | </div> | 54 | </div> |
55 | </div> | 55 | </div> |
56 | </div> | 56 | </div> |
57 | 57 | ||
58 | <h3 i18n>Federation</h3> | 58 | <h3 i18n>In this instance federation</h3> |
59 | 59 | ||
60 | <div class="row"> | 60 | <div class="row"> |
61 | <div class="col-6 col-lg-4 col-xl-3"> | 61 | <div class="col-6 col-lg-4 col-xl-3"> |
62 | <div class="card stat"> | 62 | <div class="card stat"> |
63 | <div class="card-body"> | 63 | <div class="card-body"> |
64 | <p class="stat-value">{{ serverStats.totalVideos }}</p> | 64 | <p class="stat-value">{{ serverStats.totalVideos | number }}</p> |
65 | <p class="stat-label" i18n>videos</p> | 65 | <p class="stat-label" i18n>videos</p> |
66 | </div> | 66 | </div> |
67 | <i class="glyphicon glyphicon-facetime-video"></i> | 67 | <i class="glyphicon glyphicon-facetime-video"></i> |
@@ -71,8 +71,8 @@ | |||
71 | <div class="col-6 col-lg-4 col-xl-3"> | 71 | <div class="col-6 col-lg-4 col-xl-3"> |
72 | <div class="card stat"> | 72 | <div class="card stat"> |
73 | <div class="card-body"> | 73 | <div class="card-body"> |
74 | <p class="stat-value">{{ serverStats.totalVideoComments }}</p> | 74 | <p class="stat-value">{{ serverStats.totalVideoComments | number }}</p> |
75 | <p class="stat-label" i18n>video comments</p> | 75 | <p class="stat-label" i18n>comments</p> |
76 | </div> | 76 | </div> |
77 | <i class="glyphicon glyphicon-comment"></i> | 77 | <i class="glyphicon glyphicon-comment"></i> |
78 | </div> | 78 | </div> |
@@ -81,7 +81,7 @@ | |||
81 | <div class="col-6 col-lg-4 col-xl-3"> | 81 | <div class="col-6 col-lg-4 col-xl-3"> |
82 | <div class="card stat"> | 82 | <div class="card stat"> |
83 | <div class="card-body"> | 83 | <div class="card-body"> |
84 | <p class="stat-value">{{ serverStats.totalInstanceFollowers }}</p> | 84 | <p class="stat-value">{{ serverStats.totalInstanceFollowers | number }}</p> |
85 | <p class="stat-label" i18n>followers</p> | 85 | <p class="stat-label" i18n>followers</p> |
86 | </div> | 86 | </div> |
87 | <i class="glyphicon glyphicon-retweet"></i> | 87 | <i class="glyphicon glyphicon-retweet"></i> |
@@ -91,7 +91,7 @@ | |||
91 | <div class="col-6 col-lg-4 col-xl-3"> | 91 | <div class="col-6 col-lg-4 col-xl-3"> |
92 | <div class="card stat"> | 92 | <div class="card stat"> |
93 | <div class="card-body"> | 93 | <div class="card-body"> |
94 | <p class="stat-value">{{ serverStats.totalInstanceFollowing }}</p> | 94 | <p class="stat-value">{{ serverStats.totalInstanceFollowing | number }}</p> |
95 | <p class="stat-label" i18n>following</p> | 95 | <p class="stat-label" i18n>following</p> |
96 | </div> | 96 | </div> |
97 | <i class="glyphicon glyphicon-retweet"></i> | 97 | <i class="glyphicon glyphicon-retweet"></i> |
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts index 8b78d01a6..a26a9c11c 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -17,11 +17,15 @@ export class Account extends Actor implements ServerAccount { | |||
17 | 17 | ||
18 | userId?: number | 18 | userId?: number |
19 | 19 | ||
20 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | 20 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { |
21 | return Actor.GET_ACTOR_AVATAR_URL(actor) | 21 | return Actor.GET_ACTOR_AVATAR_URL(actor, size) |
22 | } | 22 | } |
23 | 23 | ||
24 | static GET_DEFAULT_AVATAR_URL () { | 24 | static GET_DEFAULT_AVATAR_URL (size: number) { |
25 | if (size <= 48) { | ||
26 | return `${window.location.origin}/client/assets/images/default-avatar-account-48x48.png` | ||
27 | } | ||
28 | |||
25 | return `${window.location.origin}/client/assets/images/default-avatar-account.png` | 29 | return `${window.location.origin}/client/assets/images/default-avatar-account.png` |
26 | } | 30 | } |
27 | 31 | ||
@@ -42,12 +46,12 @@ export class Account extends Actor implements ServerAccount { | |||
42 | this.mutedServerByInstance = false | 46 | this.mutedServerByInstance = false |
43 | } | 47 | } |
44 | 48 | ||
45 | updateAvatar (newAvatar: ActorImage) { | 49 | updateAvatar (newAvatars: ActorImage[]) { |
46 | this.avatar = newAvatar | 50 | this.avatars = newAvatars |
47 | } | 51 | } |
48 | 52 | ||
49 | resetAvatar () { | 53 | resetAvatar () { |
50 | this.avatar = null | 54 | this.avatars = [] |
51 | } | 55 | } |
52 | 56 | ||
53 | updateBlockStatus (blockStatus: BlockStatus) { | 57 | updateBlockStatus (blockStatus: BlockStatus) { |
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 082f44fb9..bd693860d 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { getAbsoluteAPIUrl } from '@app/helpers' | 1 | import { getAbsoluteAPIUrl, getAPIHost } from '@app/helpers' |
2 | import { Actor as ServerActor, ActorImage } from '@shared/models' | 2 | import { Actor as ServerActor, ActorImage } from '@shared/models' |
3 | 3 | ||
4 | export abstract class Actor implements ServerActor { | 4 | export abstract class Actor implements ServerActor { |
@@ -13,25 +13,26 @@ export abstract class Actor implements ServerActor { | |||
13 | 13 | ||
14 | createdAt: Date | string | 14 | createdAt: Date | string |
15 | 15 | ||
16 | avatar: ActorImage | 16 | // TODO: remove, deprecated in 4.2 |
17 | avatar: never | ||
18 | |||
19 | avatars: ActorImage[] | ||
17 | 20 | ||
18 | isLocal: boolean | 21 | isLocal: boolean |
19 | 22 | ||
20 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | 23 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { |
21 | if (actor?.avatar?.url) return actor.avatar.url | 24 | const avatar = actor.avatars.sort((a, b) => a.width - b.width).find(a => a.width >= size) |
22 | 25 | ||
23 | if (actor?.avatar) { | 26 | if (!avatar) return '' |
24 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 27 | if (avatar.url) return avatar.url |
25 | 28 | ||
26 | return absoluteAPIUrl + actor.avatar.path | 29 | const absoluteAPIUrl = getAbsoluteAPIUrl() |
27 | } | ||
28 | 30 | ||
29 | return '' | 31 | return absoluteAPIUrl + avatar.path |
30 | } | 32 | } |
31 | 33 | ||
32 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { | 34 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { |
33 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 35 | const thisHost = getAPIHost() |
34 | const thisHost = new URL(absoluteAPIUrl).host | ||
35 | 36 | ||
36 | if (host.trim() === thisHost && !forceHostname) return accountName | 37 | if (host.trim() === thisHost && !forceHostname) return accountName |
37 | 38 | ||
@@ -39,8 +40,7 @@ export abstract class Actor implements ServerActor { | |||
39 | } | 40 | } |
40 | 41 | ||
41 | static IS_LOCAL (host: string) { | 42 | static IS_LOCAL (host: string) { |
42 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 43 | const thisHost = getAPIHost() |
43 | const thisHost = new URL(absoluteAPIUrl).host | ||
44 | 44 | ||
45 | return host.trim() === thisHost | 45 | return host.trim() === thisHost |
46 | } | 46 | } |
@@ -55,7 +55,7 @@ export abstract class Actor implements ServerActor { | |||
55 | 55 | ||
56 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) | 56 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
57 | 57 | ||
58 | this.avatar = hash.avatar | 58 | this.avatars = hash.avatars |
59 | this.isLocal = Actor.IS_LOCAL(this.host) | 59 | this.isLocal = Actor.IS_LOCAL(this.host) |
60 | } | 60 | } |
61 | } | 61 | } |
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts index 702475029..4f9cbc525 100644 --- a/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts +++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts | |||
@@ -19,7 +19,7 @@ export class ChannelsSetupMessageComponent implements OnInit { | |||
19 | hasChannelNotConfigured () { | 19 | hasChannelNotConfigured () { |
20 | if (!this.user.videoChannels) return false | 20 | if (!this.user.videoChannels) return false |
21 | 21 | ||
22 | return this.user.videoChannels.filter((channel: VideoChannel) => (!channel.avatar || !channel.description)).length > 0 | 22 | return this.user.videoChannels.filter((channel: VideoChannel) => (channel.avatars.length === 0 || !channel.description)).length > 0 |
23 | } | 23 | } |
24 | 24 | ||
25 | ngOnInit () { | 25 | ngOnInit () { |
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index 439547102..1eb69d5a2 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts | |||
@@ -254,11 +254,11 @@ export class UserNotification implements UserNotificationServer { | |||
254 | return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ] | 254 | return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ] |
255 | } | 255 | } |
256 | 256 | ||
257 | private setAccountAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { | 257 | private setAccountAvatarUrl (actor: { avatarUrl?: string, avatars: { width: number, url?: string, path: string }[] }) { |
258 | actor.avatarUrl = Account.GET_ACTOR_AVATAR_URL(actor) || Account.GET_DEFAULT_AVATAR_URL() | 258 | actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor, 48) || Account.GET_DEFAULT_AVATAR_URL(48) |
259 | } | 259 | } |
260 | 260 | ||
261 | private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { | 261 | private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatars: { width: number, url?: string, path: string }[] }) { |
262 | actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor) || VideoChannel.GET_DEFAULT_AVATAR_URL() | 262 | actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor, 48) || VideoChannel.GET_DEFAULT_AVATAR_URL(48) |
263 | } | 263 | } |
264 | } | 264 | } |
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts b/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts index 732f20158..129e80bc0 100644 --- a/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts +++ b/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts | |||
@@ -4,6 +4,8 @@ export interface VideoCaptionEdit { | |||
4 | label?: string | 4 | label?: string |
5 | } | 5 | } |
6 | 6 | ||
7 | action?: 'CREATE' | 'REMOVE' | 7 | action?: 'CREATE' | 'REMOVE' | 'UPDATE' |
8 | captionfile?: any | 8 | captionfile?: any |
9 | } | 9 | } |
10 | |||
11 | export type VideoCaptionWithPathEdit = VideoCaptionEdit & { captionPath?: string } | ||
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts index 97b79d842..00ebe5bc6 100644 --- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts +++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts | |||
@@ -7,6 +7,7 @@ import { objectToFormData, sortBy } from '@app/helpers' | |||
7 | import { VideoService } from '@app/shared/shared-main/video' | 7 | import { VideoService } from '@app/shared/shared-main/video' |
8 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 8 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
9 | import { ResultList, VideoCaption } from '@shared/models' | 9 | import { ResultList, VideoCaption } from '@shared/models' |
10 | import { environment } from '../../../../environments/environment' | ||
10 | import { VideoCaptionEdit } from './video-caption-edit.model' | 11 | import { VideoCaptionEdit } from './video-caption-edit.model' |
11 | 12 | ||
12 | @Injectable() | 13 | @Injectable() |
@@ -57,7 +58,7 @@ export class VideoCaptionService { | |||
57 | let obs: Observable<any> = of(undefined) | 58 | let obs: Observable<any> = of(undefined) |
58 | 59 | ||
59 | for (const videoCaption of videoCaptions) { | 60 | for (const videoCaption of videoCaptions) { |
60 | if (videoCaption.action === 'CREATE') { | 61 | if (videoCaption.action === 'CREATE' || videoCaption.action === 'UPDATE') { |
61 | obs = obs.pipe(switchMap(() => this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile))) | 62 | obs = obs.pipe(switchMap(() => this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile))) |
62 | } else if (videoCaption.action === 'REMOVE') { | 63 | } else if (videoCaption.action === 'REMOVE') { |
63 | obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id))) | 64 | obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id))) |
@@ -66,4 +67,8 @@ export class VideoCaptionService { | |||
66 | 67 | ||
67 | return obs | 68 | return obs |
68 | } | 69 | } |
70 | |||
71 | getCaptionContent ({ captionPath }: Pick<VideoCaption, 'captionPath'>) { | ||
72 | return this.authHttp.get(environment.originServerUrl + captionPath, { responseType: 'text' }) | ||
73 | } | ||
69 | } | 74 | } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index ac2679b42..e22b0cfd0 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts | |||
@@ -12,7 +12,11 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
12 | nameWithHost: string | 12 | nameWithHost: string |
13 | nameWithHostForced: string | 13 | nameWithHostForced: string |
14 | 14 | ||
15 | banner: ActorImage | 15 | // TODO: remove, deprecated in 4.2 |
16 | banner: never | ||
17 | |||
18 | banners: ActorImage[] | ||
19 | |||
16 | bannerUrl: string | 20 | bannerUrl: string |
17 | 21 | ||
18 | updatedAt: Date | string | 22 | updatedAt: Date | string |
@@ -24,23 +28,25 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
24 | 28 | ||
25 | viewsPerDay?: ViewsPerDate[] | 29 | viewsPerDay?: ViewsPerDate[] |
26 | 30 | ||
27 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | 31 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { |
28 | return Actor.GET_ACTOR_AVATAR_URL(actor) | 32 | return Actor.GET_ACTOR_AVATAR_URL(actor, size) |
29 | } | 33 | } |
30 | 34 | ||
31 | static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) { | 35 | static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) { |
32 | if (channel?.banner?.url) return channel.banner.url | 36 | if (!channel) return '' |
33 | |||
34 | if (channel?.banner) { | ||
35 | const absoluteAPIUrl = getAbsoluteAPIUrl() | ||
36 | 37 | ||
37 | return absoluteAPIUrl + channel.banner.path | 38 | const banner = channel.banners[0] |
38 | } | 39 | if (!banner) return '' |
39 | 40 | ||
40 | return '' | 41 | if (banner.url) return banner.url |
42 | return getAbsoluteAPIUrl() + banner.path | ||
41 | } | 43 | } |
42 | 44 | ||
43 | static GET_DEFAULT_AVATAR_URL () { | 45 | static GET_DEFAULT_AVATAR_URL (size: number) { |
46 | if (size <= 48) { | ||
47 | return `${window.location.origin}/client/assets/images/default-avatar-video-channel-48x48.png` | ||
48 | } | ||
49 | |||
44 | return `${window.location.origin}/client/assets/images/default-avatar-video-channel.png` | 50 | return `${window.location.origin}/client/assets/images/default-avatar-video-channel.png` |
45 | } | 51 | } |
46 | 52 | ||
@@ -51,7 +57,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
51 | this.description = hash.description | 57 | this.description = hash.description |
52 | this.support = hash.support | 58 | this.support = hash.support |
53 | 59 | ||
54 | this.banner = hash.banner | 60 | this.banners = hash.banners |
55 | 61 | ||
56 | this.isLocal = hash.isLocal | 62 | this.isLocal = hash.isLocal |
57 | 63 | ||
@@ -74,24 +80,24 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
74 | this.updateComputedAttributes() | 80 | this.updateComputedAttributes() |
75 | } | 81 | } |
76 | 82 | ||
77 | updateAvatar (newAvatar: ActorImage) { | 83 | updateAvatar (newAvatars: ActorImage[]) { |
78 | this.avatar = newAvatar | 84 | this.avatars = newAvatars |
79 | 85 | ||
80 | this.updateComputedAttributes() | 86 | this.updateComputedAttributes() |
81 | } | 87 | } |
82 | 88 | ||
83 | resetAvatar () { | 89 | resetAvatar () { |
84 | this.updateAvatar(null) | 90 | this.updateAvatar([]) |
85 | } | 91 | } |
86 | 92 | ||
87 | updateBanner (newBanner: ActorImage) { | 93 | updateBanner (newBanners: ActorImage[]) { |
88 | this.banner = newBanner | 94 | this.banners = newBanners |
89 | 95 | ||
90 | this.updateComputedAttributes() | 96 | this.updateComputedAttributes() |
91 | } | 97 | } |
92 | 98 | ||
93 | resetBanner () { | 99 | resetBanner () { |
94 | this.updateBanner(null) | 100 | this.updateBanner([]) |
95 | } | 101 | } |
96 | 102 | ||
97 | updateComputedAttributes () { | 103 | updateComputedAttributes () { |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index f37f13c51..480d250fb 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -80,7 +80,7 @@ export class VideoChannelService { | |||
80 | changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') { | 80 | changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') { |
81 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick' | 81 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick' |
82 | 82 | ||
83 | return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm) | 83 | return this.authHttp.post<{ avatars?: ActorImage[], banners?: ActorImage[] }>(url, avatarForm) |
84 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 84 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
85 | } | 85 | } |
86 | 86 | ||
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index fe5643688..8e275181c 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -84,7 +84,11 @@ export class Video implements VideoServerModel { | |||
84 | displayName: string | 84 | displayName: string |
85 | url: string | 85 | url: string |
86 | host: string | 86 | host: string |
87 | avatar?: ActorImage | 87 | |
88 | // TODO: remove, deprecated in 4.2 | ||
89 | avatar: ActorImage | ||
90 | |||
91 | avatars: ActorImage[] | ||
88 | } | 92 | } |
89 | 93 | ||
90 | channel: { | 94 | channel: { |
@@ -93,7 +97,11 @@ export class Video implements VideoServerModel { | |||
93 | displayName: string | 97 | displayName: string |
94 | url: string | 98 | url: string |
95 | host: string | 99 | host: string |
96 | avatar?: ActorImage | 100 | |
101 | // TODO: remove, deprecated in 4.2 | ||
102 | avatar: ActorImage | ||
103 | |||
104 | avatars: ActorImage[] | ||
97 | } | 105 | } |
98 | 106 | ||
99 | userHistory?: { | 107 | userHistory?: { |
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.html b/client/src/app/shared/shared-moderation/account-blocklist.component.html index 637abcb51..0143194e9 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.html +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.html | |||
@@ -33,7 +33,7 @@ | |||
33 | <td> | 33 | <td> |
34 | <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 34 | <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
35 | <div class="chip two-lines"> | 35 | <div class="chip two-lines"> |
36 | <my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar> | 36 | <my-actor-avatar [account]="accountBlock.blockedAccount" size="32"></my-actor-avatar> |
37 | <div> | 37 | <div> |
38 | {{ accountBlock.blockedAccount.displayName }} | 38 | {{ accountBlock.blockedAccount.displayName }} |
39 | <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> | 39 | <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> |
diff --git a/client/src/app/shared/shared-moderation/blocklist.service.ts b/client/src/app/shared/shared-moderation/blocklist.service.ts index f4836c6c4..3e92c2831 100644 --- a/client/src/app/shared/shared-moderation/blocklist.service.ts +++ b/client/src/app/shared/shared-moderation/blocklist.service.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { catchError, map } from 'rxjs/operators' | 2 | import { from } from 'rxjs' |
3 | import { catchError, concatMap, map, toArray } from 'rxjs/operators' | ||
3 | import { HttpClient, HttpParams } from '@angular/common/http' | 4 | import { HttpClient, HttpParams } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
5 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
@@ -120,11 +121,15 @@ export class BlocklistService { | |||
120 | ) | 121 | ) |
121 | } | 122 | } |
122 | 123 | ||
123 | blockAccountByInstance (account: Pick<Account, 'nameWithHost'>) { | 124 | blockAccountByInstance (accountsArg: Pick<Account, 'nameWithHost'> | Pick<Account, 'nameWithHost'>[]) { |
124 | const body = { accountName: account.nameWithHost } | 125 | const accounts = Array.isArray(accountsArg) ? accountsArg : [ accountsArg ] |
125 | 126 | ||
126 | return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', body) | 127 | return from(accounts) |
127 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 128 | .pipe( |
129 | concatMap(a => this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { accountName: a.nameWithHost })), | ||
130 | toArray(), | ||
131 | catchError(err => this.restExtractor.handleError(err)) | ||
132 | ) | ||
128 | } | 133 | } |
129 | 134 | ||
130 | unblockAccountByInstance (account: Pick<Account, 'nameWithHost'>) { | 135 | unblockAccountByInstance (account: Pick<Account, 'nameWithHost'>) { |
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.html b/client/src/app/shared/shared-moderation/user-ban-modal.component.html index b41ae230d..2b6726bdc 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.html +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.html | |||
@@ -1,11 +1,15 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Ban</h4> | 3 | <h4 i18n class="modal-title">{{ getModalTitle() }}</h4> |
4 | 4 | ||
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | 5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
6 | </div> | 6 | </div> |
7 | 7 | ||
8 | <div class="modal-body"> | 8 | <div class="modal-body"> |
9 | <div class="description" i18n> | ||
10 | A banned user will no longer be able to login. | ||
11 | </div> | ||
12 | |||
9 | <form novalidate [formGroup]="form" (ngSubmit)="banUser()"> | 13 | <form novalidate [formGroup]="form" (ngSubmit)="banUser()"> |
10 | <div class="form-group"> | 14 | <div class="form-group"> |
11 | <textarea | 15 | <textarea |
@@ -17,8 +21,12 @@ | |||
17 | </div> | 21 | </div> |
18 | </div> | 22 | </div> |
19 | 23 | ||
20 | <div i18n> | 24 | <div class="form-group"> |
21 | A banned user will no longer be able to login. | 25 | <my-peertube-checkbox |
26 | inputName="banMute" formControlName="mute" | ||
27 | i18n-labelText labelText="Mute to also hide videos/comments" | ||
28 | > | ||
29 | </my-peertube-checkbox> | ||
22 | </div> | 30 | </div> |
23 | 31 | ||
24 | <div class="form-group inputs"> | 32 | <div class="form-group inputs"> |
@@ -27,7 +35,7 @@ | |||
27 | (click)="hide()" (key.enter)="hide()" | 35 | (click)="hide()" (key.enter)="hide()" |
28 | > | 36 | > |
29 | 37 | ||
30 | <input type="submit" i18n-value [value]="modalMessage" class="peertube-button orange-button" [disabled]="!form.valid" /> | 38 | <input type="submit" i18n-value [value]="getModalTitle()" class="peertube-button orange-button" [disabled]="!form.valid" /> |
31 | </div> | 39 | </div> |
32 | </form> | 40 | </form> |
33 | </div> | 41 | </div> |
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.scss b/client/src/app/shared/shared-moderation/user-ban-modal.component.scss index 08e072d8f..2c46c3d03 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.scss +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.scss | |||
@@ -1,6 +1,11 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | .description { | ||
5 | font-size: 15px; | ||
6 | margin-bottom: 15px; | ||
7 | } | ||
8 | |||
4 | textarea { | 9 | textarea { |
5 | @include peertube-textarea(100%, 60px); | 10 | @include peertube-textarea(100%, 60px); |
6 | } | 11 | } |
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts index 17cad18ec..9edfac388 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { forkJoin } from 'rxjs' | ||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
@@ -5,7 +6,9 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | |||
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
6 | import { User } from '@shared/models' | 7 | import { User } from '@shared/models' |
7 | import { USER_BAN_REASON_VALIDATOR } from '../form-validators/user-validators' | 8 | import { USER_BAN_REASON_VALIDATOR } from '../form-validators/user-validators' |
9 | import { Account } from '../shared-main' | ||
8 | import { UserAdminService } from '../shared-users' | 10 | import { UserAdminService } from '../shared-users' |
11 | import { BlocklistService } from './blocklist.service' | ||
9 | 12 | ||
10 | @Component({ | 13 | @Component({ |
11 | selector: 'my-user-ban-modal', | 14 | selector: 'my-user-ban-modal', |
@@ -24,23 +27,22 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
24 | protected formValidatorService: FormValidatorService, | 27 | protected formValidatorService: FormValidatorService, |
25 | private modalService: NgbModal, | 28 | private modalService: NgbModal, |
26 | private notifier: Notifier, | 29 | private notifier: Notifier, |
27 | private userAdminService: UserAdminService | 30 | private userAdminService: UserAdminService, |
31 | private blocklistService: BlocklistService | ||
28 | ) { | 32 | ) { |
29 | super() | 33 | super() |
30 | } | 34 | } |
31 | 35 | ||
32 | ngOnInit () { | 36 | ngOnInit () { |
33 | this.buildForm({ | 37 | this.buildForm({ |
34 | reason: USER_BAN_REASON_VALIDATOR | 38 | reason: USER_BAN_REASON_VALIDATOR, |
39 | mute: null | ||
35 | }) | 40 | }) |
36 | } | 41 | } |
37 | 42 | ||
38 | openModal (user: User | User[]) { | 43 | openModal (user: User | User[]) { |
39 | this.usersToBan = user | 44 | this.usersToBan = user |
40 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | 45 | this.openedModal = this.modalService.open(this.modal, { centered: true }) |
41 | |||
42 | const isSingleUser = !(Array.isArray(this.usersToBan) && this.usersToBan.length > 1) | ||
43 | this.modalMessage = isSingleUser ? $localize`Ban this user` : $localize`Ban these users` | ||
44 | } | 46 | } |
45 | 47 | ||
46 | hide () { | 48 | hide () { |
@@ -50,8 +52,15 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
50 | 52 | ||
51 | banUser () { | 53 | banUser () { |
52 | const reason = this.form.value['reason'] || undefined | 54 | const reason = this.form.value['reason'] || undefined |
55 | const mute = this.form.value['mute'] | ||
56 | |||
57 | const observables = [ | ||
58 | this.userAdminService.banUsers(this.usersToBan, reason) | ||
59 | ] | ||
60 | |||
61 | if (mute) observables.push(this.muteAccounts()) | ||
53 | 62 | ||
54 | this.userAdminService.banUsers(this.usersToBan, reason) | 63 | forkJoin(observables) |
55 | .subscribe({ | 64 | .subscribe({ |
56 | next: () => { | 65 | next: () => { |
57 | const message = Array.isArray(this.usersToBan) | 66 | const message = Array.isArray(this.usersToBan) |
@@ -61,6 +70,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
61 | this.notifier.success(message) | 70 | this.notifier.success(message) |
62 | 71 | ||
63 | this.userBanned.emit(this.usersToBan) | 72 | this.userBanned.emit(this.usersToBan) |
73 | |||
64 | this.hide() | 74 | this.hide() |
65 | }, | 75 | }, |
66 | 76 | ||
@@ -68,4 +78,17 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
68 | }) | 78 | }) |
69 | } | 79 | } |
70 | 80 | ||
81 | getModalTitle () { | ||
82 | if (Array.isArray(this.usersToBan)) return $localize`Ban ${this.usersToBan.length} users` | ||
83 | |||
84 | return $localize`Ban "${this.usersToBan.username}"` | ||
85 | } | ||
86 | |||
87 | private muteAccounts () { | ||
88 | const accounts = Array.isArray(this.usersToBan) | ||
89 | ? this.usersToBan.map(u => new Account(u.account)) | ||
90 | : new Account(this.usersToBan.account) | ||
91 | |||
92 | return this.blocklistService.blockAccountByInstance(accounts) | ||
93 | } | ||
71 | } | 94 | } |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index 0d19565ef..787318c2c 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -7,6 +7,16 @@ import { BlocklistService } from './blocklist.service' | |||
7 | import { BulkService } from './bulk.service' | 7 | import { BulkService } from './bulk.service' |
8 | import { UserBanModalComponent } from './user-ban-modal.component' | 8 | import { UserBanModalComponent } from './user-ban-modal.component' |
9 | 9 | ||
10 | export type AccountMutedStatus = | ||
11 | Pick<Account, 'id' | 'nameWithHost' | 'host' | 'userId' | | ||
12 | 'mutedByInstance' | 'mutedByUser' | 'mutedServerByInstance' | 'mutedServerByUser'> | ||
13 | |||
14 | export type UserModerationDisplayType = { | ||
15 | myAccount?: boolean | ||
16 | instanceAccount?: boolean | ||
17 | instanceUser?: boolean | ||
18 | } | ||
19 | |||
10 | @Component({ | 20 | @Component({ |
11 | selector: 'my-user-moderation-dropdown', | 21 | selector: 'my-user-moderation-dropdown', |
12 | templateUrl: './user-moderation-dropdown.component.html' | 22 | templateUrl: './user-moderation-dropdown.component.html' |
@@ -15,8 +25,8 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
15 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent | 25 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent |
16 | 26 | ||
17 | @Input() user: User | 27 | @Input() user: User |
18 | @Input() account: Account | 28 | @Input() account: AccountMutedStatus |
19 | @Input() prependActions: DropdownAction<{ user: User, account: Account }>[] | 29 | @Input() prependActions: DropdownAction<{ user: User, account: AccountMutedStatus }>[] |
20 | 30 | ||
21 | @Input() buttonSize: 'normal' | 'small' = 'normal' | 31 | @Input() buttonSize: 'normal' | 'small' = 'normal' |
22 | @Input() buttonStyled = true | 32 | @Input() buttonStyled = true |
@@ -24,10 +34,16 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
24 | @Input() label: string | 34 | @Input() label: string |
25 | @Input() container: 'body' | undefined = undefined | 35 | @Input() container: 'body' | undefined = undefined |
26 | 36 | ||
37 | @Input() displayOptions: UserModerationDisplayType = { | ||
38 | myAccount: true, | ||
39 | instanceAccount: true, | ||
40 | instanceUser: true | ||
41 | } | ||
42 | |||
27 | @Output() userChanged = new EventEmitter() | 43 | @Output() userChanged = new EventEmitter() |
28 | @Output() userDeleted = new EventEmitter() | 44 | @Output() userDeleted = new EventEmitter() |
29 | 45 | ||
30 | userActions: DropdownAction<{ user: User, account: Account }>[][] = [] | 46 | userActions: DropdownAction<{ user: User, account: AccountMutedStatus }>[][] = [] |
31 | 47 | ||
32 | requiresEmailVerification = false | 48 | requiresEmailVerification = false |
33 | 49 | ||
@@ -111,7 +127,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
111 | }) | 127 | }) |
112 | } | 128 | } |
113 | 129 | ||
114 | blockAccountByUser (account: Account) { | 130 | blockAccountByUser (account: AccountMutedStatus) { |
115 | this.blocklistService.blockAccountByUser(account) | 131 | this.blocklistService.blockAccountByUser(account) |
116 | .subscribe({ | 132 | .subscribe({ |
117 | next: () => { | 133 | next: () => { |
@@ -125,7 +141,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
125 | }) | 141 | }) |
126 | } | 142 | } |
127 | 143 | ||
128 | unblockAccountByUser (account: Account) { | 144 | unblockAccountByUser (account: AccountMutedStatus) { |
129 | this.blocklistService.unblockAccountByUser(account) | 145 | this.blocklistService.unblockAccountByUser(account) |
130 | .subscribe({ | 146 | .subscribe({ |
131 | next: () => { | 147 | next: () => { |
@@ -167,7 +183,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
167 | }) | 183 | }) |
168 | } | 184 | } |
169 | 185 | ||
170 | blockAccountByInstance (account: Account) { | 186 | blockAccountByInstance (account: AccountMutedStatus) { |
171 | this.blocklistService.blockAccountByInstance(account) | 187 | this.blocklistService.blockAccountByInstance(account) |
172 | .subscribe({ | 188 | .subscribe({ |
173 | next: () => { | 189 | next: () => { |
@@ -181,7 +197,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
181 | }) | 197 | }) |
182 | } | 198 | } |
183 | 199 | ||
184 | unblockAccountByInstance (account: Account) { | 200 | unblockAccountByInstance (account: AccountMutedStatus) { |
185 | this.blocklistService.unblockAccountByInstance(account) | 201 | this.blocklistService.unblockAccountByInstance(account) |
186 | .subscribe({ | 202 | .subscribe({ |
187 | next: () => { | 203 | next: () => { |
@@ -246,7 +262,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
246 | return user && this.authService.getUser().id === user.id | 262 | return user && this.authService.getUser().id === user.id |
247 | } | 263 | } |
248 | 264 | ||
249 | private isMyAccount (account: Account) { | 265 | private isMyAccount (account: AccountMutedStatus) { |
250 | return account && this.authService.getUser().account.id === account.id | 266 | return account && this.authService.getUser().account.id === account.id |
251 | } | 267 | } |
252 | 268 | ||
@@ -267,9 +283,9 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
267 | } | 283 | } |
268 | 284 | ||
269 | private buildMyAccountModerationActions () { | 285 | private buildMyAccountModerationActions () { |
270 | if (!this.account || !this.authService.isLoggedIn()) return [] | 286 | if (!this.account || !this.displayOptions.myAccount || !this.authService.isLoggedIn()) return [] |
271 | 287 | ||
272 | const myAccountActions: DropdownAction<{ user: User, account: Account }>[] = [ | 288 | const myAccountActions: DropdownAction<{ user: User, account: AccountMutedStatus }>[] = [ |
273 | { | 289 | { |
274 | label: $localize`My account moderation`, | 290 | label: $localize`My account moderation`, |
275 | class: [ 'red' ], | 291 | class: [ 'red' ], |
@@ -315,9 +331,9 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
315 | 331 | ||
316 | const authUser = this.authService.getUser() | 332 | const authUser = this.authService.getUser() |
317 | 333 | ||
318 | let instanceActions: DropdownAction<{ user: User, account: Account }>[] = [] | 334 | let instanceActions: DropdownAction<{ user: User, account: AccountMutedStatus }>[] = [] |
319 | 335 | ||
320 | if (this.user && authUser.hasRight(UserRight.MANAGE_USERS) && authUser.canManage(this.user)) { | 336 | if (this.user && this.displayOptions.instanceUser && authUser.hasRight(UserRight.MANAGE_USERS) && authUser.canManage(this.user)) { |
321 | instanceActions = instanceActions.concat([ | 337 | instanceActions = instanceActions.concat([ |
322 | { | 338 | { |
323 | label: $localize`Edit user`, | 339 | label: $localize`Edit user`, |
@@ -351,7 +367,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
351 | } | 367 | } |
352 | 368 | ||
353 | // Instance actions on account blocklists | 369 | // Instance actions on account blocklists |
354 | if (this.account && authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) { | 370 | if (this.account && this.displayOptions.instanceAccount && authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) { |
355 | instanceActions = instanceActions.concat([ | 371 | instanceActions = instanceActions.concat([ |
356 | { | 372 | { |
357 | label: $localize`Mute this account`, | 373 | label: $localize`Mute this account`, |
@@ -369,7 +385,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
369 | } | 385 | } |
370 | 386 | ||
371 | // Instance actions on server blocklists | 387 | // Instance actions on server blocklists |
372 | if (this.account && authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) { | 388 | if (this.account && this.displayOptions.instanceAccount && authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) { |
373 | instanceActions = instanceActions.concat([ | 389 | instanceActions = instanceActions.concat([ |
374 | { | 390 | { |
375 | label: $localize`Mute the instance`, | 391 | label: $localize`Mute the instance`, |
@@ -386,7 +402,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
386 | ]) | 402 | ]) |
387 | } | 403 | } |
388 | 404 | ||
389 | if (this.account && authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)) { | 405 | if (this.account && this.displayOptions.instanceAccount && authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)) { |
390 | instanceActions = instanceActions.concat([ | 406 | instanceActions = instanceActions.concat([ |
391 | { | 407 | { |
392 | label: $localize`Remove comments from your instance`, | 408 | label: $localize`Remove comments from your instance`, |
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html index 4843f65b9..446ade445 100644 --- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html +++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html | |||
@@ -42,7 +42,7 @@ | |||
42 | i18n-labelText labelText="Help share videos being played" | 42 | i18n-labelText labelText="Help share videos being played" |
43 | > | 43 | > |
44 | <ng-container ngProjectAs="description"> | 44 | <ng-container ngProjectAs="description"> |
45 | <span i18n>The <a routerLink="/about/peertube" fragment="privacy">sharing system</a> implies that some technical information about your system (such as a public IP address) can be sent to other peers, but greatly helps to reduce server load.</span> | 45 | <span i18n>The <a routerLink="/about/peertube" fragment="privacy" target="_blank">sharing system</a> implies that some technical information about your system (such as a public IP address) can be sent to other peers, but greatly helps to reduce server load.</span> |
46 | </ng-container> | 46 | </ng-container> |
47 | </my-peertube-checkbox> | 47 | </my-peertube-checkbox> |
48 | </div> | 48 | </div> |
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index c2a318285..abbfc63f8 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' |
2 | import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core' | 2 | import { AuthService, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' |
3 | import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation' | 3 | import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation' |
4 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' |
5 | import { VideoCaption } from '@shared/models' | 5 | import { VideoCaption, VideoState } from '@shared/models' |
6 | import { | 6 | import { |
7 | Actor, | 7 | Actor, |
8 | DropdownAction, | 8 | DropdownAction, |
@@ -29,6 +29,7 @@ export type VideoActionsDisplayType = { | |||
29 | liveInfo?: boolean | 29 | liveInfo?: boolean |
30 | removeFiles?: boolean | 30 | removeFiles?: boolean |
31 | transcoding?: boolean | 31 | transcoding?: boolean |
32 | editor?: boolean | ||
32 | } | 33 | } |
33 | 34 | ||
34 | @Component({ | 35 | @Component({ |
@@ -59,7 +60,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
59 | mute: true, | 60 | mute: true, |
60 | liveInfo: false, | 61 | liveInfo: false, |
61 | removeFiles: false, | 62 | removeFiles: false, |
62 | transcoding: false | 63 | transcoding: false, |
64 | editor: true | ||
63 | } | 65 | } |
64 | @Input() placement = 'left' | 66 | @Input() placement = 'left' |
65 | 67 | ||
@@ -89,7 +91,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
89 | private videoBlocklistService: VideoBlockService, | 91 | private videoBlocklistService: VideoBlockService, |
90 | private screenService: ScreenService, | 92 | private screenService: ScreenService, |
91 | private videoService: VideoService, | 93 | private videoService: VideoService, |
92 | private redundancyService: RedundancyService | 94 | private redundancyService: RedundancyService, |
95 | private serverService: ServerService | ||
93 | ) { } | 96 | ) { } |
94 | 97 | ||
95 | get user () { | 98 | get user () { |
@@ -149,6 +152,12 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
149 | return this.video.isUpdatableBy(this.user) | 152 | return this.video.isUpdatableBy(this.user) |
150 | } | 153 | } |
151 | 154 | ||
155 | isVideoEditable () { | ||
156 | return this.serverService.getHTMLConfig().videoEditor.enabled && | ||
157 | this.video.state?.id === VideoState.PUBLISHED && | ||
158 | this.video.isUpdatableBy(this.user) | ||
159 | } | ||
160 | |||
152 | isVideoRemovable () { | 161 | isVideoRemovable () { |
153 | return this.video.isRemovableBy(this.user) | 162 | return this.video.isRemovableBy(this.user) |
154 | } | 163 | } |
@@ -330,6 +339,12 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
330 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.update && this.isVideoUpdatable() | 339 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.update && this.isVideoUpdatable() |
331 | }, | 340 | }, |
332 | { | 341 | { |
342 | label: $localize`Editor`, | ||
343 | linkBuilder: ({ video }) => [ '/video-editor/edit', video.uuid ], | ||
344 | iconName: 'film', | ||
345 | isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.editor && this.isVideoEditable() | ||
346 | }, | ||
347 | { | ||
333 | label: $localize`Block`, | 348 | label: $localize`Block`, |
334 | handler: () => this.showBlockModal(), | 349 | handler: () => this.showBlockModal(), |
335 | iconName: 'no', | 350 | iconName: 'no', |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 30483831a..3cf128de0 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -13,11 +13,13 @@ | |||
13 | <my-actor-avatar | 13 | <my-actor-avatar |
14 | *ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle" | 14 | *ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle" |
15 | [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" | 15 | [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" |
16 | size="32" | ||
16 | ></my-actor-avatar> | 17 | ></my-actor-avatar> |
17 | 18 | ||
18 | <my-actor-avatar | 19 | <my-actor-avatar |
19 | *ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle" | 20 | *ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle" |
20 | [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" | 21 | [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" |
22 | size="32" | ||
21 | ></my-actor-avatar> | 23 | ></my-actor-avatar> |
22 | 24 | ||
23 | <div class="w-100 d-flex flex-column"> | 25 | <div class="w-100 d-flex flex-column"> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 847e401ed..7de9fc8e2 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -195,6 +195,10 @@ export class VideoMiniatureComponent implements OnInit { | |||
195 | return $localize`To import` | 195 | return $localize`To import` |
196 | } | 196 | } |
197 | 197 | ||
198 | if (video.state.id === VideoState.TO_EDIT) { | ||
199 | return $localize`To edit` | ||
200 | } | ||
201 | |||
198 | return '' | 202 | return '' |
199 | } | 203 | } |
200 | 204 | ||