diff options
Diffstat (limited to 'client/src/app')
32 files changed, 582 insertions, 211 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 27d5e0a10..b8a957d1c 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -44,6 +44,14 @@ export class AdminComponent implements OnInit { | |||
44 | }) | 44 | }) |
45 | } | 45 | } |
46 | 46 | ||
47 | if (this.hasVideosRight()) { | ||
48 | overviewItems.children.push({ | ||
49 | label: $localize`Videos`, | ||
50 | routerLink: '/admin/videos', | ||
51 | iconName: 'videos' | ||
52 | }) | ||
53 | } | ||
54 | |||
47 | if (overviewItems.children.length !== 0) { | 55 | if (overviewItems.children.length !== 0) { |
48 | this.menuEntries.push(overviewItems) | 56 | this.menuEntries.push(overviewItems) |
49 | } | 57 | } |
@@ -217,4 +225,8 @@ export class AdminComponent implements OnInit { | |||
217 | private hasVideoCommentsRight () { | 225 | private hasVideoCommentsRight () { |
218 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS) | 226 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS) |
219 | } | 227 | } |
228 | |||
229 | private hasVideosRight () { | ||
230 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) | ||
231 | } | ||
220 | } | 232 | } |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index a2bd88880..d04c11a20 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -10,7 +10,9 @@ import { SharedFormModule } from '@app/shared/shared-forms' | |||
10 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 10 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
11 | import { SharedMainModule } from '@app/shared/shared-main' | 11 | import { SharedMainModule } from '@app/shared/shared-main' |
12 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 12 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
13 | import { SharedTablesModule } from '@app/shared/shared-tables' | ||
13 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' | 14 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' |
15 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | ||
14 | import { AdminRoutingModule } from './admin-routing.module' | 16 | import { AdminRoutingModule } from './admin-routing.module' |
15 | import { AdminComponent } from './admin.component' | 17 | import { AdminComponent } from './admin.component' |
16 | import { | 18 | import { |
@@ -33,7 +35,7 @@ import { AbuseListComponent, VideoBlockListComponent } from './moderation' | |||
33 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' | 35 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' |
34 | import { ModerationComponent } from './moderation/moderation.component' | 36 | import { ModerationComponent } from './moderation/moderation.component' |
35 | import { VideoCommentListComponent } from './moderation/video-comment-list' | 37 | import { VideoCommentListComponent } from './moderation/video-comment-list' |
36 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UserUpdateComponent } from './overview' | 38 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UserUpdateComponent, VideoListComponent } from './overview' |
37 | import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' | 39 | import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' |
38 | import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' | 40 | import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' |
39 | import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' | 41 | import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' |
@@ -56,6 +58,8 @@ import { JobsComponent } from './system/jobs/jobs.component' | |||
56 | SharedActorImageModule, | 58 | SharedActorImageModule, |
57 | SharedActorImageEditModule, | 59 | SharedActorImageEditModule, |
58 | SharedCustomMarkupModule, | 60 | SharedCustomMarkupModule, |
61 | SharedVideoMiniatureModule, | ||
62 | SharedTablesModule, | ||
59 | 63 | ||
60 | TableModule, | 64 | TableModule, |
61 | SelectButtonModule, | 65 | SelectButtonModule, |
@@ -65,6 +69,8 @@ import { JobsComponent } from './system/jobs/jobs.component' | |||
65 | declarations: [ | 69 | declarations: [ |
66 | AdminComponent, | 70 | AdminComponent, |
67 | 71 | ||
72 | VideoListComponent, | ||
73 | |||
68 | FollowsComponent, | 74 | FollowsComponent, |
69 | FollowersListComponent, | 75 | FollowersListComponent, |
70 | FollowingListComponent, | 76 | FollowingListComponent, |
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html index 7efa87dd0..3cd69cfbc 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html | |||
@@ -34,9 +34,7 @@ | |||
34 | <tr> | 34 | <tr> |
35 | <td *ngIf="!videoBlock.reason"></td> | 35 | <td *ngIf="!videoBlock.reason"></td> |
36 | <td *ngIf="videoBlock.reason" class="expand-cell c-hand" [pRowToggler]="videoBlock" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> | 36 | <td *ngIf="videoBlock.reason" class="expand-cell c-hand" [pRowToggler]="videoBlock" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> |
37 | <span class="expander"> | 37 | <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon> |
38 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | ||
39 | </span> | ||
40 | </td> | 38 | </td> |
41 | 39 | ||
42 | <td class="action-cell"> | 40 | <td class="action-cell"> |
@@ -47,22 +45,11 @@ | |||
47 | </td> | 45 | </td> |
48 | 46 | ||
49 | <td> | 47 | <td> |
50 | <a [href]="getVideoUrl(videoBlock)" class="table-video-link" [title]="videoBlock.video.name" target="_blank" rel="noopener noreferrer"> | 48 | <my-video-cell [video]="videoBlock.video"> |
51 | <div class="table-video"> | 49 | <span name> |
52 | <div class="table-video-image"> | 50 | <my-global-icon *ngIf="videoBlock.type === 2" i18n-title title="The video was blocked due to automatic blocking of new videos" iconName="robot"></my-global-icon> |
53 | <img [src]="videoBlock.video.thumbnailPath"> | 51 | </span> |
54 | </div> | 52 | </my-video-cell> |
55 | |||
56 | <div class="table-video-text"> | ||
57 | <div> | ||
58 | <my-global-icon i18n-title title="The video was blocked due to automatic blocking of new videos" *ngIf="videoBlock.type === 2" iconName="robot"></my-global-icon> | ||
59 | {{ videoBlock.video.name }} | ||
60 | </div> | ||
61 | |||
62 | <div class="text-muted">by {{ videoBlock.video.channel?.displayName }} on {{ videoBlock.video.channel?.host }} </div> | ||
63 | </div> | ||
64 | </div> | ||
65 | </a> | ||
66 | </td> | 53 | </td> |
67 | 54 | ||
68 | <td> | 55 | <td> |
@@ -90,9 +77,7 @@ | |||
90 | </div> | 77 | </div> |
91 | 78 | ||
92 | <div class="right"> | 79 | <div class="right"> |
93 | <div class="screenratio"> | 80 | <my-embed [video]="videoBlock.video"></my-embed> |
94 | <div [innerHTML]="videoBlock.embedHtml"></div> | ||
95 | </div> | ||
96 | </div> | 81 | </div> |
97 | 82 | ||
98 | </div> | 83 | </div> |
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 7baf34ca2..1fe8d0f9d 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -3,11 +3,10 @@ import { switchMap } from 'rxjs/operators' | |||
3 | import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | 3 | import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' |
4 | import { environment } from 'src/environments/environment' | 4 | import { environment } from 'src/environments/environment' |
5 | import { Component, OnInit } from '@angular/core' | 5 | import { Component, OnInit } from '@angular/core' |
6 | import { DomSanitizer } from '@angular/platform-browser' | ||
7 | import { ActivatedRoute, Router } from '@angular/router' | 6 | import { ActivatedRoute, Router } from '@angular/router' |
8 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 7 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 8 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 9 | import { DropdownAction, VideoService } from '@app/shared/shared-main' |
11 | import { VideoBlockService } from '@app/shared/shared-moderation' | 10 | import { VideoBlockService } from '@app/shared/shared-moderation' |
12 | import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils' | 11 | import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils' |
13 | import { VideoBlacklist, VideoBlacklistType } from '@shared/models' | 12 | import { VideoBlacklist, VideoBlacklistType } from '@shared/models' |
@@ -18,7 +17,7 @@ import { VideoBlacklist, VideoBlacklistType } from '@shared/models' | |||
18 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ] | 17 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ] |
19 | }) | 18 | }) |
20 | export class VideoBlockListComponent extends RestTable implements OnInit { | 19 | export class VideoBlockListComponent extends RestTable implements OnInit { |
21 | blocklist: (VideoBlacklist & { reasonHtml?: string, embedHtml?: string })[] = [] | 20 | blocklist: (VideoBlacklist & { reasonHtml?: string })[] = [] |
22 | totalRecords = 0 | 21 | totalRecords = 0 |
23 | sort: SortMeta = { field: 'createdAt', order: -1 } | 22 | sort: SortMeta = { field: 'createdAt', order: -1 } |
24 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
@@ -50,7 +49,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit { | |||
50 | private confirmService: ConfirmService, | 49 | private confirmService: ConfirmService, |
51 | private videoBlocklistService: VideoBlockService, | 50 | private videoBlocklistService: VideoBlockService, |
52 | private markdownRenderer: MarkdownService, | 51 | private markdownRenderer: MarkdownService, |
53 | private sanitizer: DomSanitizer, | ||
54 | private videoService: VideoService | 52 | private videoService: VideoService |
55 | ) { | 53 | ) { |
56 | super() | 54 | super() |
@@ -125,10 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit { | |||
125 | return 'VideoBlockListComponent' | 123 | return 'VideoBlockListComponent' |
126 | } | 124 | } |
127 | 125 | ||
128 | getVideoUrl (videoBlock: VideoBlacklist) { | ||
129 | return Video.buildWatchUrl(videoBlock.video) | ||
130 | } | ||
131 | |||
132 | toHtml (text: string) { | 126 | toHtml (text: string) { |
133 | return this.markdownRenderer.textMarkdownToHTML(text) | 127 | return this.markdownRenderer.textMarkdownToHTML(text) |
134 | } | 128 | } |
@@ -176,8 +170,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit { | |||
176 | 170 | ||
177 | for (const element of this.blocklist) { | 171 | for (const element of this.blocklist) { |
178 | Object.assign(element, { | 172 | Object.assign(element, { |
179 | reasonHtml: await this.toHtml(element.reason), | 173 | reasonHtml: await this.toHtml(element.reason) |
180 | embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(element)) | ||
181 | }) | 174 | }) |
182 | } | 175 | } |
183 | }, | 176 | }, |
diff --git a/client/src/app/+admin/overview/index.ts b/client/src/app/+admin/overview/index.ts index b71a6a45f..a9c46893f 100644 --- a/client/src/app/+admin/overview/index.ts +++ b/client/src/app/+admin/overview/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './users' | 1 | export * from './users' |
2 | export * from './videos' | ||
2 | export * from './overview.routes' | 3 | 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 cb5986072..1e6686d16 100644 --- a/client/src/app/+admin/overview/overview.routes.ts +++ b/client/src/app/+admin/overview/overview.routes.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | import { UsersRoutes } from './users' | 2 | import { UsersRoutes } from './users' |
3 | import { VideosRoutes } from './videos' | ||
3 | 4 | ||
4 | export const OverviewRoutes: Routes = [ | 5 | export const OverviewRoutes: Routes = [ |
5 | ...UsersRoutes | 6 | ...UsersRoutes, |
7 | ...VideosRoutes | ||
6 | ] | 8 | ] |
diff --git a/client/src/app/+admin/overview/videos/index.ts b/client/src/app/+admin/overview/videos/index.ts new file mode 100644 index 000000000..40c2ffe72 --- /dev/null +++ b/client/src/app/+admin/overview/videos/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-list.component' | ||
2 | export * from './video.routes' | ||
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html new file mode 100644 index 000000000..1f1e9cc6e --- /dev/null +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -0,0 +1,86 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>Videos</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <p-table | ||
7 | [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
8 | [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedVideos" | ||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | ||
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos" | ||
12 | (onPage)="onPage($event)" [expandedRowKeys]="expandedRows" | ||
13 | > | ||
14 | <ng-template pTemplate="caption"> | ||
15 | <div class="caption"> | ||
16 | <div class="left-buttons"> | ||
17 | <my-action-dropdown | ||
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | ||
19 | [actions]="bulkVideoActions" [entry]="selectedVideos" | ||
20 | > | ||
21 | </my-action-dropdown> | ||
22 | </div> | ||
23 | |||
24 | <div class="ml-auto"> | ||
25 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> | ||
26 | </div> | ||
27 | |||
28 | </div> | ||
29 | </ng-template> | ||
30 | |||
31 | <ng-template pTemplate="header"> | ||
32 | <tr> | ||
33 | <th style="width: 40px"> | ||
34 | <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox> | ||
35 | </th> | ||
36 | <th style="width: 40px"></th> | ||
37 | <th style="width: 60px;"></th> | ||
38 | <th i18n>Video</th> | ||
39 | <th i18n>Info</th> | ||
40 | <th style="width: 150px;" i18n pSortableColumn="publishedAt">Published <p-sortIcon field="publishedAt"></p-sortIcon></th> | ||
41 | </tr> | ||
42 | </ng-template> | ||
43 | |||
44 | <ng-template pTemplate="body" let-expanded="expanded" let-video> | ||
45 | |||
46 | <tr [pSelectableRow]="video"> | ||
47 | <td class="checkbox-cell"> | ||
48 | <p-tableCheckbox [value]="video" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox> | ||
49 | </td> | ||
50 | |||
51 | <td class="expand-cell" [pRowToggler]="video"> | ||
52 | <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon> | ||
53 | </td> | ||
54 | |||
55 | <td class="action-cell"> | ||
56 | <my-video-actions-dropdown | ||
57 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" | ||
58 | [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" | ||
59 | ></my-video-actions-dropdown> | ||
60 | </td> | ||
61 | |||
62 | <td> | ||
63 | <my-video-cell [video]="video"></my-video-cell> | ||
64 | </td> | ||
65 | |||
66 | <td> | ||
67 | <span class="badge badge-blue" i18n>{{ video.privacy.label }}</span> | ||
68 | <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span> | ||
69 | <span *ngIf="video.blocked" class="badge badge-red" i18n>NSFW</span> | ||
70 | </td> | ||
71 | |||
72 | <td> | ||
73 | {{ video.publishedAt | date: 'short' }} | ||
74 | </td> | ||
75 | |||
76 | </tr> | ||
77 | </ng-template> | ||
78 | |||
79 | <ng-template pTemplate="rowexpansion" let-video> | ||
80 | <tr> | ||
81 | <td colspan="50"> | ||
82 | <my-embed [video]="video"></my-embed> | ||
83 | </td> | ||
84 | </tr> | ||
85 | </ng-template> | ||
86 | </p-table> | ||
diff --git a/client/src/app/+admin/overview/videos/video-list.component.scss b/client/src/app/+admin/overview/videos/video-list.component.scss new file mode 100644 index 000000000..fcdb457f2 --- /dev/null +++ b/client/src/app/+admin/overview/videos/video-list.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | my-embed { | ||
4 | display: block; | ||
5 | max-width: 500px; | ||
6 | } | ||
7 | |||
8 | .badge { | ||
9 | @include table-badge; | ||
10 | } | ||
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts new file mode 100644 index 000000000..a445bc209 --- /dev/null +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -0,0 +1,123 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | ||
5 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | ||
6 | import { UserRight } from '@shared/models' | ||
7 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
8 | import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-video-list', | ||
12 | templateUrl: './video-list.component.html', | ||
13 | styleUrls: [ './video-list.component.scss' ] | ||
14 | }) | ||
15 | export class VideoListComponent extends RestTable implements OnInit { | ||
16 | videos: Video[] = [] | ||
17 | |||
18 | totalRecords = 0 | ||
19 | sort: SortMeta = { field: 'publishedAt', order: 1 } | ||
20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
21 | |||
22 | bulkVideoActions: DropdownAction<Video[]>[][] = [] | ||
23 | |||
24 | selectedVideos: Video[] = [] | ||
25 | |||
26 | inputFilters: AdvancedInputFilter[] = [ | ||
27 | { | ||
28 | title: $localize`Advanced filters`, | ||
29 | children: [ | ||
30 | { | ||
31 | queryParams: { search: 'local:true' }, | ||
32 | label: $localize`Only local videos` | ||
33 | } | ||
34 | ] | ||
35 | } | ||
36 | ] | ||
37 | |||
38 | videoActionsOptions: VideoActionsDisplayType = { | ||
39 | playlist: false, | ||
40 | download: false, | ||
41 | update: true, | ||
42 | blacklist: true, | ||
43 | delete: true, | ||
44 | report: false, | ||
45 | duplicate: true, | ||
46 | mute: true, | ||
47 | liveInfo: false | ||
48 | } | ||
49 | |||
50 | constructor ( | ||
51 | protected route: ActivatedRoute, | ||
52 | protected router: Router, | ||
53 | private confirmService: ConfirmService, | ||
54 | private auth: AuthService, | ||
55 | private notifier: Notifier, | ||
56 | private videoService: VideoService | ||
57 | ) { | ||
58 | super() | ||
59 | } | ||
60 | |||
61 | get authUser () { | ||
62 | return this.auth.getUser() | ||
63 | } | ||
64 | |||
65 | ngOnInit () { | ||
66 | this.initialize() | ||
67 | |||
68 | this.bulkVideoActions = [ | ||
69 | [ | ||
70 | { | ||
71 | label: $localize`Delete`, | ||
72 | handler: videos => this.removeVideos(videos), | ||
73 | isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO) | ||
74 | } | ||
75 | ] | ||
76 | ] | ||
77 | } | ||
78 | |||
79 | getIdentifier () { | ||
80 | return 'VideoListComponent' | ||
81 | } | ||
82 | |||
83 | isInSelectionMode () { | ||
84 | return this.selectedVideos.length !== 0 | ||
85 | } | ||
86 | |||
87 | onVideoRemoved () { | ||
88 | this.reloadData() | ||
89 | } | ||
90 | |||
91 | protected reloadData () { | ||
92 | this.selectedVideos = [] | ||
93 | |||
94 | this.videoService.getAdminVideos({ | ||
95 | pagination: this.pagination, | ||
96 | sort: this.sort, | ||
97 | search: this.search | ||
98 | }).subscribe({ | ||
99 | next: resultList => { | ||
100 | this.videos = resultList.data | ||
101 | this.totalRecords = resultList.total | ||
102 | }, | ||
103 | |||
104 | error: err => this.notifier.error(err.message) | ||
105 | }) | ||
106 | } | ||
107 | |||
108 | private async removeVideos (videos: Video[]) { | ||
109 | const message = $localize`Are you sure you want to delete these ${videos.length} videos?` | ||
110 | const res = await this.confirmService.confirm(message, $localize`Delete`) | ||
111 | if (res === false) return | ||
112 | |||
113 | this.videoService.removeVideo(videos.map(v => v.id)) | ||
114 | .subscribe({ | ||
115 | next: () => { | ||
116 | this.notifier.success($localize`${videos.length} videos deleted.`) | ||
117 | this.reloadData() | ||
118 | }, | ||
119 | |||
120 | error: err => this.notifier.error(err.message) | ||
121 | }) | ||
122 | } | ||
123 | } | ||
diff --git a/client/src/app/+admin/overview/videos/video.routes.ts b/client/src/app/+admin/overview/videos/video.routes.ts new file mode 100644 index 000000000..984df7b82 --- /dev/null +++ b/client/src/app/+admin/overview/videos/video.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 { VideoListComponent } from './video-list.component' | ||
5 | |||
6 | export const VideosRoutes: Routes = [ | ||
7 | { | ||
8 | path: 'videos', | ||
9 | canActivate: [ UserRightGuard ], | ||
10 | data: { | ||
11 | userRight: UserRight.SEE_ALL_VIDEOS | ||
12 | }, | ||
13 | children: [ | ||
14 | { | ||
15 | path: '', | ||
16 | redirectTo: 'list', | ||
17 | pathMatch: 'full' | ||
18 | }, | ||
19 | { | ||
20 | path: 'list', | ||
21 | component: VideoListComponent, | ||
22 | data: { | ||
23 | meta: { | ||
24 | title: $localize`Videos list` | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | ] | ||
29 | } | ||
30 | ] | ||
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html index 2f0bc5ae5..a1a4586f0 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html | |||
@@ -85,9 +85,9 @@ | |||
85 | 85 | ||
86 | <!-- report right part (video/comment details) --> | 86 | <!-- report right part (video/comment details) --> |
87 | <div class="right"> | 87 | <div class="right"> |
88 | <div *ngIf="abuse.video" class="screenratio"> | 88 | <div *ngIf="abuse.video"> |
89 | <div *ngIf="abuse.video.deleted" i18n>The video was deleted</div> | 89 | <div *ngIf="abuse.video.deleted" i18n>The video was deleted</div> |
90 | <div *ngIf="!abuse.video.deleted" [innerHTML]="abuse.embedHtml"></div> | 90 | <my-embed *ngIf="!abuse.video.deleted" [video]="abuse.video"></my-embed> |
91 | </div> | 91 | </div> |
92 | 92 | ||
93 | <div *ngIf="abuse.comment" class="comment-html"> | 93 | <div *ngIf="abuse.comment" class="comment-html"> |
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 4bf83316b..d957eaeab 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 | |||
@@ -30,9 +30,7 @@ | |||
30 | <ng-template pTemplate="body" let-expanded="expanded" let-abuse> | 30 | <ng-template pTemplate="body" let-expanded="expanded" let-abuse> |
31 | <tr> | 31 | <tr> |
32 | <td class="expand-cell c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> | 32 | <td class="expand-cell c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> |
33 | <span class="expander"> | 33 | <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon> |
34 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | ||
35 | </span> | ||
36 | </td> | 34 | </td> |
37 | 35 | ||
38 | <td class="action-cell"> | 36 | <td class="action-cell"> |
@@ -61,28 +59,20 @@ | |||
61 | <ng-container *ngIf="abuse.video"> | 59 | <ng-container *ngIf="abuse.video"> |
62 | 60 | ||
63 | <td *ngIf="!abuse.video.deleted"> | 61 | <td *ngIf="!abuse.video.deleted"> |
64 | <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer"> | 62 | <my-video-cell [video]="abuse.video"> |
65 | <div class="table-video"> | 63 | <span image> |
66 | <div class="table-video-image"> | 64 | <span |
67 | <img [src]="abuse.video.thumbnailPath"> | 65 | class="table-video-image-label" *ngIf="abuse.count > 1" |
68 | <span | 66 | i18n-title title="This video has been reported multiple times." |
69 | class="table-video-image-label" *ngIf="abuse.count > 1" | 67 | > |
70 | i18n-title title="This video has been reported multiple times." | 68 | {{ abuse.nth }}/{{ abuse.count }} |
71 | > | 69 | </span> |
72 | {{ abuse.nth }}/{{ abuse.count }} | 70 | </span> |
73 | </span> | 71 | |
74 | </div> | 72 | <span name> |
75 | 73 | <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> | |
76 | <div class="table-video-text"> | 74 | </span> |
77 | <div> | 75 | </my-video-cell> |
78 | <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> | ||
79 | <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> | ||
80 | {{ abuse.video.name }} | ||
81 | </div> | ||
82 | <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> | ||
83 | </div> | ||
84 | </div> | ||
85 | </a> | ||
86 | </td> | 76 | </td> |
87 | 77 | ||
88 | <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse"> | 78 | <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse"> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 297993e39..10f5861b9 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -1,8 +1,6 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import truncate from 'lodash-es/truncate' | 2 | import truncate from 'lodash-es/truncate' |
3 | import { SortMeta } from 'primeng/api' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | ||
5 | import { environment } from 'src/environments/environment' | ||
6 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
7 | import { DomSanitizer } from '@angular/platform-browser' | 5 | import { DomSanitizer } from '@angular/platform-browser' |
8 | import { ActivatedRoute, Router } from '@angular/router' | 6 | import { ActivatedRoute, Router } from '@angular/router' |
@@ -10,7 +8,6 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } | |||
10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
11 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' | 9 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' |
12 | import { VideoCommentService } from '@app/shared/shared-video-comment' | 10 | import { VideoCommentService } from '@app/shared/shared-video-comment' |
13 | import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils' | ||
14 | import { AbuseState, AdminAbuse } from '@shared/models' | 11 | import { AbuseState, AdminAbuse } from '@shared/models' |
15 | import { AdvancedInputFilter } from '../shared-forms' | 12 | import { AdvancedInputFilter } from '../shared-forms' |
16 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' | 13 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' |
@@ -133,19 +130,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
133 | return '/a/' + abuse.flaggedAccount.nameWithHost | 130 | return '/a/' + abuse.flaggedAccount.nameWithHost |
134 | } | 131 | } |
135 | 132 | ||
136 | getVideoEmbed (abuse: AdminAbuse) { | ||
137 | return buildVideoOrPlaylistEmbed( | ||
138 | decorateVideoLink({ | ||
139 | url: buildVideoEmbedLink(abuse.video, environment.originServerUrl), | ||
140 | title: false, | ||
141 | warningTitle: false, | ||
142 | startTime: abuse.video.startAt, | ||
143 | stopTime: abuse.video.endAt | ||
144 | }), | ||
145 | abuse.video.name | ||
146 | ) | ||
147 | } | ||
148 | |||
149 | async removeAbuse (abuse: AdminAbuse) { | 133 | async removeAbuse (abuse: AdminAbuse) { |
150 | const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`) | 134 | const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`) |
151 | if (res === false) return | 135 | if (res === false) return |
@@ -220,8 +204,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
220 | } | 204 | } |
221 | 205 | ||
222 | if (abuse.video) { | 206 | if (abuse.video) { |
223 | abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)) | ||
224 | |||
225 | if (abuse.video.channel?.ownerAccount) { | 207 | if (abuse.video.channel?.ownerAccount) { |
226 | abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) | 208 | abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) |
227 | } | 209 | } |
diff --git a/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts b/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts index 194d52a33..b9a9bd889 100644 --- a/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts +++ b/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { SafeHtml } from '@angular/platform-browser' | ||
2 | import { AdminAbuse } from '@shared/models' | ||
3 | import { Account } from '@app/shared/shared-main' | 1 | import { Account } from '@app/shared/shared-main' |
2 | import { AdminAbuse } from '@shared/models' | ||
4 | 3 | ||
5 | // Don't use an abuse model because we need external services to compute some properties | 4 | // Don't use an abuse model because we need external services to compute some properties |
6 | // And this model is only used in this component | 5 | // And this model is only used in this component |
7 | export type ProcessedAbuse = AdminAbuse & { | 6 | export type ProcessedAbuse = AdminAbuse & { |
8 | moderationCommentHtml?: string | 7 | moderationCommentHtml?: string |
9 | reasonHtml?: string | 8 | reasonHtml?: string |
10 | embedHtml?: SafeHtml | ||
11 | updatedAt?: Date | 9 | updatedAt?: Date |
12 | 10 | ||
13 | // override bare server-side definitions with rich client-side definitions | 11 | // override bare server-side definitions with rich client-side definitions |
diff --git a/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts b/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts index 8f3830a17..eeda27fa6 100644 --- a/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts +++ b/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts | |||
@@ -1,16 +1,17 @@ | |||
1 | 1 | ||
2 | import { TableModule } from 'primeng/table' | 2 | import { TableModule } from 'primeng/table' |
3 | import { NgModule } from '@angular/core' | 3 | import { NgModule } from '@angular/core' |
4 | import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' | ||
4 | import { SharedFormModule } from '../shared-forms/shared-form.module' | 5 | import { SharedFormModule } from '../shared-forms/shared-form.module' |
5 | import { SharedGlobalIconModule } from '../shared-icons' | 6 | import { SharedGlobalIconModule } from '../shared-icons' |
6 | import { SharedMainModule } from '../shared-main/shared-main.module' | 7 | import { SharedMainModule } from '../shared-main/shared-main.module' |
7 | import { SharedModerationModule } from '../shared-moderation' | 8 | import { SharedModerationModule } from '../shared-moderation' |
9 | import { SharedTablesModule } from '../shared-tables' | ||
8 | import { SharedVideoCommentModule } from '../shared-video-comment' | 10 | import { SharedVideoCommentModule } from '../shared-video-comment' |
9 | import { AbuseDetailsComponent } from './abuse-details.component' | 11 | import { AbuseDetailsComponent } from './abuse-details.component' |
10 | import { AbuseListTableComponent } from './abuse-list-table.component' | 12 | import { AbuseListTableComponent } from './abuse-list-table.component' |
11 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' | 13 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' |
12 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | 14 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' |
13 | import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' | ||
14 | 15 | ||
15 | @NgModule({ | 16 | @NgModule({ |
16 | imports: [ | 17 | imports: [ |
@@ -21,7 +22,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image | |||
21 | SharedModerationModule, | 22 | SharedModerationModule, |
22 | SharedGlobalIconModule, | 23 | SharedGlobalIconModule, |
23 | SharedVideoCommentModule, | 24 | SharedVideoCommentModule, |
24 | SharedActorImageModule | 25 | SharedActorImageModule, |
26 | SharedTablesModule | ||
25 | ], | 27 | ], |
26 | 28 | ||
27 | declarations: [ | 29 | declarations: [ |
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 93989780d..a90b59e41 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -43,13 +43,8 @@ import { | |||
43 | } from './misc' | 43 | } from './misc' |
44 | import { PluginPlaceholderComponent } from './plugins' | 44 | import { PluginPlaceholderComponent } from './plugins' |
45 | import { ActorRedirectGuard } from './router' | 45 | import { ActorRedirectGuard } from './router' |
46 | import { | 46 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
47 | UserHistoryService, | 47 | import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' |
48 | UserNotificationsComponent, | ||
49 | UserNotificationService, | ||
50 | UserQuotaComponent | ||
51 | } from './users' | ||
52 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | ||
53 | import { VideoCaptionService } from './video-caption' | 48 | import { VideoCaptionService } from './video-caption' |
54 | import { VideoChannelService } from './video-channel' | 49 | import { VideoChannelService } from './video-channel' |
55 | 50 | ||
@@ -111,6 +106,8 @@ import { VideoChannelService } from './video-channel' | |||
111 | UserQuotaComponent, | 106 | UserQuotaComponent, |
112 | UserNotificationsComponent, | 107 | UserNotificationsComponent, |
113 | 108 | ||
109 | EmbedComponent, | ||
110 | |||
114 | PluginPlaceholderComponent | 111 | PluginPlaceholderComponent |
115 | ], | 112 | ], |
116 | 113 | ||
@@ -167,6 +164,8 @@ import { VideoChannelService } from './video-channel' | |||
167 | UserQuotaComponent, | 164 | UserQuotaComponent, |
168 | UserNotificationsComponent, | 165 | UserNotificationsComponent, |
169 | 166 | ||
167 | EmbedComponent, | ||
168 | |||
170 | PluginPlaceholderComponent | 169 | PluginPlaceholderComponent |
171 | ], | 170 | ], |
172 | 171 | ||
diff --git a/client/src/app/shared/shared-main/video/embed.component.html b/client/src/app/shared/shared-main/video/embed.component.html new file mode 100644 index 000000000..3b088d058 --- /dev/null +++ b/client/src/app/shared/shared-main/video/embed.component.html | |||
@@ -0,0 +1,3 @@ | |||
1 | <div class="screenratio"> | ||
2 | <div [innerHTML]="embedHTML"></div> | ||
3 | </div> | ||
diff --git a/client/src/app/shared/shared-main/video/embed.component.scss b/client/src/app/shared/shared-main/video/embed.component.scss new file mode 100644 index 000000000..420ba6f23 --- /dev/null +++ b/client/src/app/shared/shared-main/video/embed.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @use '_mixins' as *; | ||
2 | @use '_variables' as *; | ||
3 | |||
4 | .screenratio { | ||
5 | @include block-ratio($selector: 'div, ::ng-deep iframe') { | ||
6 | width: 100% !important; | ||
7 | height: 100% !important; | ||
8 | left: 0; | ||
9 | }; | ||
10 | } | ||
diff --git a/client/src/app/shared/shared-main/video/embed.component.ts b/client/src/app/shared/shared-main/video/embed.component.ts new file mode 100644 index 000000000..4732efa44 --- /dev/null +++ b/client/src/app/shared/shared-main/video/embed.component.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | ||
2 | import { environment } from 'src/environments/environment' | ||
3 | import { Component, Input, OnInit } from '@angular/core' | ||
4 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | ||
5 | import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils' | ||
6 | import { Video } from '@shared/models' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-embed', | ||
10 | styleUrls: [ './embed.component.scss' ], | ||
11 | templateUrl: './embed.component.html' | ||
12 | }) | ||
13 | export class EmbedComponent implements OnInit { | ||
14 | @Input() video: Pick<Video, 'name' | 'uuid'> | ||
15 | |||
16 | embedHTML: SafeHtml | ||
17 | |||
18 | constructor (private sanitizer: DomSanitizer) { | ||
19 | |||
20 | } | ||
21 | |||
22 | ngOnInit () { | ||
23 | const html = buildVideoOrPlaylistEmbed( | ||
24 | decorateVideoLink({ | ||
25 | url: buildVideoEmbedLink(this.video, environment.originServerUrl), | ||
26 | |||
27 | title: false, | ||
28 | warningTitle: false | ||
29 | }), | ||
30 | this.video.name | ||
31 | ) | ||
32 | |||
33 | this.embedHTML = this.sanitizer.bypassSecurityTrustHtml(html) | ||
34 | } | ||
35 | } | ||
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 3053df4ef..e72c0c3d6 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './embed.component' | ||
1 | export * from './redundancy.service' | 2 | export * from './redundancy.service' |
2 | export * from './video-details.model' | 3 | export * from './video-details.model' |
3 | export * from './video-edit.model' | 4 | export * from './video-edit.model' |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 7935569e7..9e3aa1e6a 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Observable } from 'rxjs' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { catchError, map, switchMap } from 'rxjs/operators' | 2 | import { from, Observable } from 'rxjs' |
3 | import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' | ||
3 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | 4 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
5 | import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' | 6 | import { ComponentPaginationLight, RestExtractor, RestPagination, RestService, ServerService, UserService } from '@app/core' |
6 | import { objectToFormData } from '@app/helpers' | 7 | import { objectToFormData } from '@app/helpers' |
7 | import { | 8 | import { |
8 | BooleanBothQuery, | 9 | BooleanBothQuery, |
@@ -31,8 +32,8 @@ import { VideoEdit } from './video-edit.model' | |||
31 | import { Video } from './video.model' | 32 | import { Video } from './video.model' |
32 | 33 | ||
33 | export type CommonVideoParams = { | 34 | export type CommonVideoParams = { |
34 | videoPagination: ComponentPaginationLight | 35 | videoPagination?: ComponentPaginationLight |
35 | sort: VideoSortField | 36 | sort: VideoSortField | SortMeta |
36 | filter?: VideoFilter | 37 | filter?: VideoFilter |
37 | categoryOneOf?: number[] | 38 | categoryOneOf?: number[] |
38 | languageOneOf?: string[] | 39 | languageOneOf?: string[] |
@@ -200,6 +201,31 @@ export class VideoService { | |||
200 | ) | 201 | ) |
201 | } | 202 | } |
202 | 203 | ||
204 | getAdminVideos ( | ||
205 | parameters: Omit<CommonVideoParams, 'filter'> & { pagination: RestPagination, search?: string } | ||
206 | ): Observable<ResultList<Video>> { | ||
207 | const { pagination, search } = parameters | ||
208 | |||
209 | let params = new HttpParams() | ||
210 | params = this.buildCommonVideosParams({ params, ...parameters }) | ||
211 | |||
212 | params = params.set('start', pagination.start.toString()) | ||
213 | .set('count', pagination.count.toString()) | ||
214 | |||
215 | if (search) { | ||
216 | params = this.buildAdminParamsFromSearch(search, params) | ||
217 | } | ||
218 | |||
219 | if (!params.has('filter')) params = params.set('filter', 'all') | ||
220 | |||
221 | return this.authHttp | ||
222 | .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params }) | ||
223 | .pipe( | ||
224 | switchMap(res => this.extractVideos(res)), | ||
225 | catchError(err => this.restExtractor.handleError(err)) | ||
226 | ) | ||
227 | } | ||
228 | |||
203 | getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> { | 229 | getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> { |
204 | let params = new HttpParams() | 230 | let params = new HttpParams() |
205 | params = this.buildCommonVideosParams({ params, ...parameters }) | 231 | params = this.buildCommonVideosParams({ params, ...parameters }) |
@@ -284,13 +310,15 @@ export class VideoService { | |||
284 | ) | 310 | ) |
285 | } | 311 | } |
286 | 312 | ||
287 | removeVideo (id: number) { | 313 | removeVideo (idArg: number | number[]) { |
288 | return this.authHttp | 314 | const ids = Array.isArray(idArg) ? idArg : [ idArg ] |
289 | .delete(VideoService.BASE_VIDEO_URL + id) | 315 | |
290 | .pipe( | 316 | return from(ids) |
291 | map(this.restExtractor.extractDataBool), | 317 | .pipe( |
292 | catchError(err => this.restExtractor.handleError(err)) | 318 | concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)), |
293 | ) | 319 | toArray(), |
320 | catchError(err => this.restExtractor.handleError(err)) | ||
321 | ) | ||
294 | } | 322 | } |
295 | 323 | ||
296 | loadCompleteDescription (descriptionPath: string) { | 324 | loadCompleteDescription (descriptionPath: string) { |
@@ -393,9 +421,23 @@ export class VideoService { | |||
393 | } | 421 | } |
394 | 422 | ||
395 | private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) { | 423 | private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) { |
396 | const { params, videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive, nsfw } = options | 424 | const { |
425 | params, | ||
426 | videoPagination, | ||
427 | sort, | ||
428 | filter, | ||
429 | categoryOneOf, | ||
430 | languageOneOf, | ||
431 | skipCount, | ||
432 | nsfwPolicy, | ||
433 | isLive, | ||
434 | nsfw | ||
435 | } = options | ||
436 | |||
437 | const pagination = videoPagination | ||
438 | ? this.restService.componentToRestPagination(videoPagination) | ||
439 | : undefined | ||
397 | 440 | ||
398 | const pagination = this.restService.componentToRestPagination(videoPagination) | ||
399 | let newParams = this.restService.addRestGetParams(params, pagination, sort) | 441 | let newParams = this.restService.addRestGetParams(params, pagination, sort) |
400 | 442 | ||
401 | if (filter) newParams = newParams.set('filter', filter) | 443 | if (filter) newParams = newParams.set('filter', filter) |
@@ -409,4 +451,19 @@ export class VideoService { | |||
409 | 451 | ||
410 | return newParams | 452 | return newParams |
411 | } | 453 | } |
454 | |||
455 | private buildAdminParamsFromSearch (search: string, params: HttpParams) { | ||
456 | const filters = this.restService.parseQueryStringFilter(search, { | ||
457 | filter: { | ||
458 | prefix: 'local:', | ||
459 | handler: v => { | ||
460 | if (v === 'true') return 'all-local' | ||
461 | |||
462 | return 'all' | ||
463 | } | ||
464 | } | ||
465 | }) | ||
466 | |||
467 | return this.restService.addObjectParams(params, filters) | ||
468 | } | ||
412 | } | 469 | } |
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index 815e2791f..eaf5a8250 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss | |||
@@ -40,14 +40,6 @@ | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | .screenratio { | ||
44 | @include block-ratio($selector: 'div, ::ng-deep iframe') { | ||
45 | width: 100% !important; | ||
46 | height: 100% !important; | ||
47 | left: 0; | ||
48 | }; | ||
49 | } | ||
50 | |||
51 | .chip { | 43 | .chip { |
52 | @include chip; | 44 | @include chip; |
53 | } | 45 | } |
@@ -58,13 +50,6 @@ my-action-dropdown.show { | |||
58 | } | 50 | } |
59 | } | 51 | } |
60 | 52 | ||
61 | .table-video-link { | ||
62 | @include disable-outline; | ||
63 | |||
64 | position: relative; | ||
65 | top: 3px; | ||
66 | } | ||
67 | |||
68 | .table-comment-link, | 53 | .table-comment-link, |
69 | .table-account-link { | 54 | .table-account-link { |
70 | @include disable-outline; | 55 | @include disable-outline; |
@@ -81,68 +66,6 @@ my-action-dropdown.show { | |||
81 | flex-direction: column; | 66 | flex-direction: column; |
82 | } | 67 | } |
83 | 68 | ||
84 | .table-video { | ||
85 | display: inline-flex; | ||
86 | |||
87 | .table-video-image { | ||
88 | $image-height: 45px; | ||
89 | |||
90 | @include miniature-thumbnail; | ||
91 | @include margin-right(0.5rem); | ||
92 | |||
93 | height: $image-height; | ||
94 | width: #{math.div(16, 9) * $image-height}; | ||
95 | border-radius: 2px; | ||
96 | border: 0; | ||
97 | background: transparent; | ||
98 | display: inline-flex; | ||
99 | justify-content: center; | ||
100 | position: relative; | ||
101 | |||
102 | img { | ||
103 | height: 100%; | ||
104 | width: 100%; | ||
105 | border-radius: 2px; | ||
106 | } | ||
107 | |||
108 | span { | ||
109 | color: pvar(--inputPlaceholderColor); | ||
110 | } | ||
111 | |||
112 | .table-video-image-label { | ||
113 | @include static-thumbnail-overlay; | ||
114 | position: absolute; | ||
115 | border-radius: 3px; | ||
116 | font-size: 10px; | ||
117 | padding: 0 3px; | ||
118 | line-height: 1.3; | ||
119 | bottom: 2px; | ||
120 | right: 2px; | ||
121 | } | ||
122 | } | ||
123 | |||
124 | .table-video-text { | ||
125 | display: inline-flex; | ||
126 | flex-direction: column; | ||
127 | justify-content: center; | ||
128 | font-size: 90%; | ||
129 | color: pvar(--mainForegroundColor); | ||
130 | line-height: 1rem; | ||
131 | |||
132 | div .glyphicon { | ||
133 | @include margin-left(0.1rem); | ||
134 | |||
135 | font-size: 80%; | ||
136 | color: #808080; | ||
137 | } | ||
138 | |||
139 | div + div { | ||
140 | color: var(--greyForegroundColor); | ||
141 | font-size: 11px; | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | |||
146 | my-abuse-details { | 69 | my-abuse-details { |
147 | width: 100%; | 70 | width: 100%; |
148 | } | 71 | } |
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss index 06e50ac2d..76ec0a6ed 100644 --- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss +++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss | |||
@@ -19,9 +19,3 @@ textarea { | |||
19 | @include margin-left(10px); | 19 | @include margin-left(10px); |
20 | } | 20 | } |
21 | } | 21 | } |
22 | |||
23 | .screenratio { | ||
24 | @include block-ratio($selector: 'div, ::ng-deep iframe') { | ||
25 | left: 0; | ||
26 | }; | ||
27 | } | ||
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html index 1aae64bff..afac108fc 100644 --- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html +++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html | |||
@@ -35,9 +35,7 @@ | |||
35 | <div class="col-7"> | 35 | <div class="col-7"> |
36 | <div class="row justify-content-center"> | 36 | <div class="row justify-content-center"> |
37 | <div class="col-12 col-lg-9 mb-2"> | 37 | <div class="col-12 col-lg-9 mb-2"> |
38 | <div class="screenratio"> | 38 | <my-embed [video]="video"></my-embed> |
39 | <div [innerHTML]="embedHtml"></div> | ||
40 | </div> | ||
41 | </div> | 39 | </div> |
42 | </div> | 40 | </div> |
43 | 41 | ||
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts index 278d60ac6..38dd92910 100644 --- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { mapValues, pickBy } from 'lodash-es' | 1 | import { mapValues, pickBy } from 'lodash-es' |
2 | import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | ||
3 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
4 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | 3 | import { DomSanitizer } from '@angular/platform-browser' |
5 | import { Notifier } from '@app/core' | 4 | import { Notifier } from '@app/core' |
6 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' | 5 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' |
7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
9 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
10 | import { decorateVideoLink } from '@shared/core-utils' | ||
11 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 9 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' |
12 | import { AbusePredefinedReasonsString } from '@shared/models' | 10 | import { AbusePredefinedReasonsString } from '@shared/models' |
13 | import { Video } from '../../shared-main' | 11 | import { Video } from '../../shared-main' |
@@ -25,7 +23,6 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
25 | 23 | ||
26 | error: string = null | 24 | error: string = null |
27 | predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | 25 | predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] |
28 | embedHtml: SafeHtml | ||
29 | 26 | ||
30 | private openedModal: NgbModalRef | 27 | private openedModal: NgbModalRef |
31 | 28 | ||
@@ -55,20 +52,6 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
55 | return this.form.get('timestamp').value | 52 | return this.form.get('timestamp').value |
56 | } | 53 | } |
57 | 54 | ||
58 | getVideoEmbed () { | ||
59 | return this.sanitizer.bypassSecurityTrustHtml( | ||
60 | buildVideoOrPlaylistEmbed( | ||
61 | decorateVideoLink({ | ||
62 | url: this.video.embedUrl, | ||
63 | title: false, | ||
64 | warningTitle: false | ||
65 | }), | ||
66 | |||
67 | this.video.name | ||
68 | ) | ||
69 | ) | ||
70 | } | ||
71 | |||
72 | ngOnInit () { | 55 | ngOnInit () { |
73 | this.buildForm({ | 56 | this.buildForm({ |
74 | reason: ABUSE_REASON_VALIDATOR, | 57 | reason: ABUSE_REASON_VALIDATOR, |
@@ -82,8 +65,6 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
82 | }) | 65 | }) |
83 | 66 | ||
84 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('video') | 67 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('video') |
85 | |||
86 | this.embedHtml = this.getVideoEmbed() | ||
87 | } | 68 | } |
88 | 69 | ||
89 | show () { | 70 | show () { |
diff --git a/client/src/app/shared/shared-tables/index.ts b/client/src/app/shared/shared-tables/index.ts new file mode 100644 index 000000000..e7b593257 --- /dev/null +++ b/client/src/app/shared/shared-tables/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './table-expander-icon.component' | ||
2 | export * from './video-cell.component' | ||
3 | export * from './shared-tables.module' | ||
diff --git a/client/src/app/shared/shared-tables/shared-tables.module.ts b/client/src/app/shared/shared-tables/shared-tables.module.ts new file mode 100644 index 000000000..c528365a0 --- /dev/null +++ b/client/src/app/shared/shared-tables/shared-tables.module.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | |||
2 | import { NgModule } from '@angular/core' | ||
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | ||
4 | import { TableExpanderIconComponent } from './table-expander-icon.component' | ||
5 | import { VideoCellComponent } from './video-cell.component' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | SharedMainModule | ||
10 | ], | ||
11 | |||
12 | declarations: [ | ||
13 | VideoCellComponent, | ||
14 | TableExpanderIconComponent | ||
15 | ], | ||
16 | |||
17 | exports: [ | ||
18 | VideoCellComponent, | ||
19 | TableExpanderIconComponent | ||
20 | ], | ||
21 | |||
22 | providers: [ | ||
23 | ] | ||
24 | }) | ||
25 | export class SharedTablesModule { } | ||
diff --git a/client/src/app/shared/shared-tables/table-expander-icon.component.ts b/client/src/app/shared/shared-tables/table-expander-icon.component.ts new file mode 100644 index 000000000..3756b475a --- /dev/null +++ b/client/src/app/shared/shared-tables/table-expander-icon.component.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-table-expander-icon', | ||
5 | template: ` | ||
6 | <span class="expander"> | ||
7 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | ||
8 | </span>` | ||
9 | }) | ||
10 | export class TableExpanderIconComponent { | ||
11 | @Input() expanded: boolean | ||
12 | } | ||
diff --git a/client/src/app/shared/shared-tables/video-cell.component.html b/client/src/app/shared/shared-tables/video-cell.component.html new file mode 100644 index 000000000..fb7d852ab --- /dev/null +++ b/client/src/app/shared/shared-tables/video-cell.component.html | |||
@@ -0,0 +1,19 @@ | |||
1 | <a [href]="getVideoUrl()" class="table-video-link" [title]="video.name" target="_blank" rel="noopener noreferrer"> | ||
2 | <div class="table-video"> | ||
3 | <div class="table-video-image"> | ||
4 | <img [src]="video.thumbnailPath"> | ||
5 | |||
6 | <ng-content select="[image]"></ng-content> | ||
7 | </div> | ||
8 | |||
9 | <div class="table-video-text"> | ||
10 | <div> | ||
11 | <ng-content select="[name]"></ng-content> | ||
12 | |||
13 | {{ video.name }} | ||
14 | </div> | ||
15 | |||
16 | <div class="text-muted">by {{ video.channel?.displayName }} on {{ video.channel?.host }} </div> | ||
17 | </div> | ||
18 | </div> | ||
19 | </a> | ||
diff --git a/client/src/app/shared/shared-tables/video-cell.component.scss b/client/src/app/shared/shared-tables/video-cell.component.scss new file mode 100644 index 000000000..7efb61502 --- /dev/null +++ b/client/src/app/shared/shared-tables/video-cell.component.scss | |||
@@ -0,0 +1,74 @@ | |||
1 | @use 'sass:math'; | ||
2 | @use '_mixins' as *; | ||
3 | @use '_variables' as *; | ||
4 | @use '_miniature' as *; | ||
5 | |||
6 | .table-video-link { | ||
7 | @include disable-outline; | ||
8 | |||
9 | position: relative; | ||
10 | top: 3px; | ||
11 | } | ||
12 | |||
13 | .table-video { | ||
14 | display: inline-flex; | ||
15 | |||
16 | .table-video-image { | ||
17 | $image-height: 45px; | ||
18 | |||
19 | @include miniature-thumbnail; | ||
20 | @include margin-right(0.5rem); | ||
21 | |||
22 | height: $image-height; | ||
23 | width: #{math.div(16, 9) * $image-height}; | ||
24 | border-radius: 2px; | ||
25 | border: 0; | ||
26 | background: transparent; | ||
27 | display: inline-flex; | ||
28 | justify-content: center; | ||
29 | position: relative; | ||
30 | |||
31 | img { | ||
32 | height: 100%; | ||
33 | width: 100%; | ||
34 | border-radius: 2px; | ||
35 | } | ||
36 | |||
37 | span { | ||
38 | color: pvar(--inputPlaceholderColor); | ||
39 | } | ||
40 | |||
41 | .table-video-image-label { | ||
42 | @include static-thumbnail-overlay; | ||
43 | |||
44 | position: absolute; | ||
45 | border-radius: 3px; | ||
46 | font-size: 10px; | ||
47 | padding: 0 3px; | ||
48 | line-height: 1.3; | ||
49 | bottom: 2px; | ||
50 | right: 2px; | ||
51 | } | ||
52 | } | ||
53 | |||
54 | .table-video-text { | ||
55 | display: inline-flex; | ||
56 | flex-direction: column; | ||
57 | justify-content: center; | ||
58 | font-size: 90%; | ||
59 | color: pvar(--mainForegroundColor); | ||
60 | line-height: 1rem; | ||
61 | |||
62 | div .glyphicon { | ||
63 | @include margin-left(0.1rem); | ||
64 | |||
65 | font-size: 80%; | ||
66 | color: #808080; | ||
67 | } | ||
68 | |||
69 | div + div { | ||
70 | color: var(--greyForegroundColor); | ||
71 | font-size: 11px; | ||
72 | } | ||
73 | } | ||
74 | } | ||
diff --git a/client/src/app/shared/shared-tables/video-cell.component.ts b/client/src/app/shared/shared-tables/video-cell.component.ts new file mode 100644 index 000000000..62984180d --- /dev/null +++ b/client/src/app/shared/shared-tables/video-cell.component.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { Video } from '@app/shared/shared-main' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-video-cell', | ||
6 | styleUrls: [ 'video-cell.component.scss' ], | ||
7 | templateUrl: 'video-cell.component.html' | ||
8 | }) | ||
9 | export class VideoCellComponent { | ||
10 | @Input() video: Video | ||
11 | |||
12 | getVideoUrl () { | ||
13 | return Video.buildWatchUrl(this.video) | ||
14 | } | ||
15 | } | ||