Add videos list admin component

pull/4510/head
Chocobozzz 2021-10-27 11:42:05 +02:00 committed by Chocobozzz
parent 00004f7f6b
commit 33f6dce136
32 changed files with 581 additions and 210 deletions

View File

@ -44,6 +44,14 @@ export class AdminComponent implements OnInit {
}) })
} }
if (this.hasVideosRight()) {
overviewItems.children.push({
label: $localize`Videos`,
routerLink: '/admin/videos',
iconName: 'videos'
})
}
if (overviewItems.children.length !== 0) { if (overviewItems.children.length !== 0) {
this.menuEntries.push(overviewItems) this.menuEntries.push(overviewItems)
} }
@ -217,4 +225,8 @@ export class AdminComponent implements OnInit {
private hasVideoCommentsRight () { private hasVideoCommentsRight () {
return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS) return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS)
} }
private hasVideosRight () {
return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
}
} }

View File

@ -10,7 +10,9 @@ import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons' import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main' import { SharedMainModule } from '@app/shared/shared-main'
import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedModerationModule } from '@app/shared/shared-moderation'
import { SharedTablesModule } from '@app/shared/shared-tables'
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
import { AdminRoutingModule } from './admin-routing.module' import { AdminRoutingModule } from './admin-routing.module'
import { AdminComponent } from './admin.component' import { AdminComponent } from './admin.component'
import { import {
@ -33,7 +35,7 @@ import { AbuseListComponent, VideoBlockListComponent } from './moderation'
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
import { ModerationComponent } from './moderation/moderation.component' import { ModerationComponent } from './moderation/moderation.component'
import { VideoCommentListComponent } from './moderation/video-comment-list' import { VideoCommentListComponent } from './moderation/video-comment-list'
import { UserCreateComponent, UserListComponent, UserPasswordComponent, UserUpdateComponent } from './overview' import { UserCreateComponent, UserListComponent, UserPasswordComponent, UserUpdateComponent, VideoListComponent } from './overview'
import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
@ -56,6 +58,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
SharedActorImageModule, SharedActorImageModule,
SharedActorImageEditModule, SharedActorImageEditModule,
SharedCustomMarkupModule, SharedCustomMarkupModule,
SharedVideoMiniatureModule,
SharedTablesModule,
TableModule, TableModule,
SelectButtonModule, SelectButtonModule,
@ -65,6 +69,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
declarations: [ declarations: [
AdminComponent, AdminComponent,
VideoListComponent,
FollowsComponent, FollowsComponent,
FollowersListComponent, FollowersListComponent,
FollowingListComponent, FollowingListComponent,

View File

@ -34,9 +34,7 @@
<tr> <tr>
<td *ngIf="!videoBlock.reason"></td> <td *ngIf="!videoBlock.reason"></td>
<td *ngIf="videoBlock.reason" class="expand-cell c-hand" [pRowToggler]="videoBlock" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> <td *ngIf="videoBlock.reason" class="expand-cell c-hand" [pRowToggler]="videoBlock" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
<span class="expander"> <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span>
</td> </td>
<td class="action-cell"> <td class="action-cell">
@ -47,22 +45,11 @@
</td> </td>
<td> <td>
<a [href]="getVideoUrl(videoBlock)" class="table-video-link" [title]="videoBlock.video.name" target="_blank" rel="noopener noreferrer"> <my-video-cell [video]="videoBlock.video">
<div class="table-video"> <span name>
<div class="table-video-image"> <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>
<img [src]="videoBlock.video.thumbnailPath"> </span>
</div> </my-video-cell>
<div class="table-video-text">
<div>
<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>
{{ videoBlock.video.name }}
</div>
<div class="text-muted">by {{ videoBlock.video.channel?.displayName }} on {{ videoBlock.video.channel?.host }} </div>
</div>
</div>
</a>
</td> </td>
<td> <td>
@ -90,9 +77,7 @@
</div> </div>
<div class="right"> <div class="right">
<div class="screenratio"> <my-embed [video]="videoBlock.video"></my-embed>
<div [innerHTML]="videoBlock.embedHtml"></div>
</div>
</div> </div>
</div> </div>

View File

@ -3,11 +3,10 @@ import { switchMap } from 'rxjs/operators'
import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
import { AdvancedInputFilter } from '@app/shared/shared-forms' import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' import { DropdownAction, VideoService } from '@app/shared/shared-main'
import { VideoBlockService } from '@app/shared/shared-moderation' import { VideoBlockService } from '@app/shared/shared-moderation'
import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils' import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
import { VideoBlacklist, VideoBlacklistType } from '@shared/models' import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
@ -18,7 +17,7 @@ import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ] styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ]
}) })
export class VideoBlockListComponent extends RestTable implements OnInit { export class VideoBlockListComponent extends RestTable implements OnInit {
blocklist: (VideoBlacklist & { reasonHtml?: string, embedHtml?: string })[] = [] blocklist: (VideoBlacklist & { reasonHtml?: string })[] = []
totalRecords = 0 totalRecords = 0
sort: SortMeta = { field: 'createdAt', order: -1 } sort: SortMeta = { field: 'createdAt', order: -1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
@ -50,7 +49,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
private confirmService: ConfirmService, private confirmService: ConfirmService,
private videoBlocklistService: VideoBlockService, private videoBlocklistService: VideoBlockService,
private markdownRenderer: MarkdownService, private markdownRenderer: MarkdownService,
private sanitizer: DomSanitizer,
private videoService: VideoService private videoService: VideoService
) { ) {
super() super()
@ -125,10 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
return 'VideoBlockListComponent' return 'VideoBlockListComponent'
} }
getVideoUrl (videoBlock: VideoBlacklist) {
return Video.buildWatchUrl(videoBlock.video)
}
toHtml (text: string) { toHtml (text: string) {
return this.markdownRenderer.textMarkdownToHTML(text) return this.markdownRenderer.textMarkdownToHTML(text)
} }
@ -176,8 +170,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
for (const element of this.blocklist) { for (const element of this.blocklist) {
Object.assign(element, { Object.assign(element, {
reasonHtml: await this.toHtml(element.reason), reasonHtml: await this.toHtml(element.reason)
embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(element))
}) })
} }
}, },

View File

@ -1,2 +1,3 @@
export * from './users' export * from './users'
export * from './videos'
export * from './overview.routes' export * from './overview.routes'

View File

@ -1,6 +1,8 @@
import { Routes } from '@angular/router' import { Routes } from '@angular/router'
import { UsersRoutes } from './users' import { UsersRoutes } from './users'
import { VideosRoutes } from './videos'
export const OverviewRoutes: Routes = [ export const OverviewRoutes: Routes = [
...UsersRoutes ...UsersRoutes,
...VideosRoutes
] ]

View File

@ -0,0 +1,2 @@
export * from './video-list.component'
export * from './video.routes'

View File

@ -0,0 +1,86 @@
<h1>
<my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
<ng-container i18n>Videos</ng-container>
</h1>
<p-table
[value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
[sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedVideos"
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos"
(onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
>
<ng-template pTemplate="caption">
<div class="caption">
<div class="left-buttons">
<my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
[actions]="bulkVideoActions" [entry]="selectedVideos"
>
</my-action-dropdown>
</div>
<div class="ml-auto">
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
</div>
</div>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th style="width: 40px">
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
</th>
<th style="width: 40px"></th>
<th style="width: 60px;"></th>
<th i18n>Video</th>
<th i18n>Info</th>
<th style="width: 150px;" i18n pSortableColumn="publishedAt">Published <p-sortIcon field="publishedAt"></p-sortIcon></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-expanded="expanded" let-video>
<tr [pSelectableRow]="video">
<td class="checkbox-cell">
<p-tableCheckbox [value]="video" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
</td>
<td class="expand-cell" [pRowToggler]="video">
<my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
</td>
<td class="action-cell">
<my-video-actions-dropdown
placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video"
[displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()"
></my-video-actions-dropdown>
</td>
<td>
<my-video-cell [video]="video"></my-video-cell>
</td>
<td>
<span class="badge badge-blue" i18n>{{ video.privacy.label }}</span>
<span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span>
<span *ngIf="video.blocked" class="badge badge-red" i18n>NSFW</span>
</td>
<td>
{{ video.publishedAt | date: 'short' }}
</td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-video>
<tr>
<td colspan="50">
<my-embed [video]="video"></my-embed>
</td>
</tr>
</ng-template>
</p-table>

View File

@ -0,0 +1,10 @@
@use '_variables' as *;
@use '_mixins' as *;
my-embed {
display: block;
max-width: 500px;
}
.badge {
@include table-badge;
}

View File

@ -0,0 +1,123 @@
import { SortMeta } from 'primeng/api'
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { UserRight } from '@shared/models'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
@Component({
selector: 'my-video-list',
templateUrl: './video-list.component.html',
styleUrls: [ './video-list.component.scss' ]
})
export class VideoListComponent extends RestTable implements OnInit {
videos: Video[] = []
totalRecords = 0
sort: SortMeta = { field: 'publishedAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
bulkVideoActions: DropdownAction<Video[]>[][] = []
selectedVideos: Video[] = []
inputFilters: AdvancedInputFilter[] = [
{
title: $localize`Advanced filters`,
children: [
{
queryParams: { search: 'local:true' },
label: $localize`Only local videos`
}
]
}
]
videoActionsOptions: VideoActionsDisplayType = {
playlist: false,
download: false,
update: true,
blacklist: true,
delete: true,
report: false,
duplicate: true,
mute: true,
liveInfo: false
}
constructor (
protected route: ActivatedRoute,
protected router: Router,
private confirmService: ConfirmService,
private auth: AuthService,
private notifier: Notifier,
private videoService: VideoService
) {
super()
}
get authUser () {
return this.auth.getUser()
}
ngOnInit () {
this.initialize()
this.bulkVideoActions = [
[
{
label: $localize`Delete`,
handler: videos => this.removeVideos(videos),
isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO)
}
]
]
}
getIdentifier () {
return 'VideoListComponent'
}
isInSelectionMode () {
return this.selectedVideos.length !== 0
}
onVideoRemoved () {
this.reloadData()
}
protected reloadData () {
this.selectedVideos = []
this.videoService.getAdminVideos({
pagination: this.pagination,
sort: this.sort,
search: this.search
}).subscribe({
next: resultList => {
this.videos = resultList.data
this.totalRecords = resultList.total
},
error: err => this.notifier.error(err.message)
})
}
private async removeVideos (videos: Video[]) {
const message = $localize`Are you sure you want to delete these ${videos.length} videos?`
const res = await this.confirmService.confirm(message, $localize`Delete`)
if (res === false) return
this.videoService.removeVideo(videos.map(v => v.id))
.subscribe({
next: () => {
this.notifier.success($localize`${videos.length} videos deleted.`)
this.reloadData()
},
error: err => this.notifier.error(err.message)
})
}
}

View File

@ -0,0 +1,30 @@
import { Routes } from '@angular/router'
import { UserRightGuard } from '@app/core'
import { UserRight } from '@shared/models'
import { VideoListComponent } from './video-list.component'
export const VideosRoutes: Routes = [
{
path: 'videos',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.SEE_ALL_VIDEOS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: VideoListComponent,
data: {
meta: {
title: $localize`Videos list`
}
}
}
]
}
]

View File

@ -85,9 +85,9 @@
<!-- report right part (video/comment details) --> <!-- report right part (video/comment details) -->
<div class="right"> <div class="right">
<div *ngIf="abuse.video" class="screenratio"> <div *ngIf="abuse.video">
<div *ngIf="abuse.video.deleted" i18n>The video was deleted</div> <div *ngIf="abuse.video.deleted" i18n>The video was deleted</div>
<div *ngIf="!abuse.video.deleted" [innerHTML]="abuse.embedHtml"></div> <my-embed *ngIf="!abuse.video.deleted" [video]="abuse.video"></my-embed>
</div> </div>
<div *ngIf="abuse.comment" class="comment-html"> <div *ngIf="abuse.comment" class="comment-html">

View File

@ -30,9 +30,7 @@
<ng-template pTemplate="body" let-expanded="expanded" let-abuse> <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
<tr> <tr>
<td class="expand-cell c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> <td class="expand-cell c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
<span class="expander"> <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span>
</td> </td>
<td class="action-cell"> <td class="action-cell">
@ -61,28 +59,20 @@
<ng-container *ngIf="abuse.video"> <ng-container *ngIf="abuse.video">
<td *ngIf="!abuse.video.deleted"> <td *ngIf="!abuse.video.deleted">
<a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer"> <my-video-cell [video]="abuse.video">
<div class="table-video"> <span image>
<div class="table-video-image">
<img [src]="abuse.video.thumbnailPath">
<span <span
class="table-video-image-label" *ngIf="abuse.count > 1" class="table-video-image-label" *ngIf="abuse.count > 1"
i18n-title title="This video has been reported multiple times." i18n-title title="This video has been reported multiple times."
> >
{{ abuse.nth }}/{{ abuse.count }} {{ abuse.nth }}/{{ abuse.count }}
</span> </span>
</div> </span>
<div class="table-video-text"> <span name>
<div>
<span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
<span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
{{ abuse.video.name }} </span>
</div> </my-video-cell>
<div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
</div>
</div>
</a>
</td> </td>
<td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse"> <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">

View File

@ -1,8 +1,6 @@
import * as debug from 'debug' import * as debug from 'debug'
import truncate from 'lodash-es/truncate' import truncate from 'lodash-es/truncate'
import { SortMeta } from 'primeng/api' import { SortMeta } from 'primeng/api'
import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment'
import { Component, Input, OnInit, ViewChild } from '@angular/core' import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser' import { DomSanitizer } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
@ -10,7 +8,6 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable }
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
import { VideoCommentService } from '@app/shared/shared-video-comment' import { VideoCommentService } from '@app/shared/shared-video-comment'
import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
import { AbuseState, AdminAbuse } from '@shared/models' import { AbuseState, AdminAbuse } from '@shared/models'
import { AdvancedInputFilter } from '../shared-forms' import { AdvancedInputFilter } from '../shared-forms'
import { AbuseMessageModalComponent } from './abuse-message-modal.component' import { AbuseMessageModalComponent } from './abuse-message-modal.component'
@ -133,19 +130,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
return '/a/' + abuse.flaggedAccount.nameWithHost return '/a/' + abuse.flaggedAccount.nameWithHost
} }
getVideoEmbed (abuse: AdminAbuse) {
return buildVideoOrPlaylistEmbed(
decorateVideoLink({
url: buildVideoEmbedLink(abuse.video, environment.originServerUrl),
title: false,
warningTitle: false,
startTime: abuse.video.startAt,
stopTime: abuse.video.endAt
}),
abuse.video.name
)
}
async removeAbuse (abuse: AdminAbuse) { async removeAbuse (abuse: AdminAbuse) {
const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`) const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`)
if (res === false) return if (res === false) return
@ -220,8 +204,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
} }
if (abuse.video) { if (abuse.video) {
abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
if (abuse.video.channel?.ownerAccount) { if (abuse.video.channel?.ownerAccount) {
abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
} }

View File

@ -1,13 +1,11 @@
import { SafeHtml } from '@angular/platform-browser'
import { AdminAbuse } from '@shared/models'
import { Account } from '@app/shared/shared-main' import { Account } from '@app/shared/shared-main'
import { AdminAbuse } from '@shared/models'
// Don't use an abuse model because we need external services to compute some properties // Don't use an abuse model because we need external services to compute some properties
// And this model is only used in this component // And this model is only used in this component
export type ProcessedAbuse = AdminAbuse & { export type ProcessedAbuse = AdminAbuse & {
moderationCommentHtml?: string moderationCommentHtml?: string
reasonHtml?: string reasonHtml?: string
embedHtml?: SafeHtml
updatedAt?: Date updatedAt?: Date
// override bare server-side definitions with rich client-side definitions // override bare server-side definitions with rich client-side definitions

View File

@ -1,16 +1,17 @@
import { TableModule } from 'primeng/table' import { TableModule } from 'primeng/table'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
import { SharedFormModule } from '../shared-forms/shared-form.module' import { SharedFormModule } from '../shared-forms/shared-form.module'
import { SharedGlobalIconModule } from '../shared-icons' import { SharedGlobalIconModule } from '../shared-icons'
import { SharedMainModule } from '../shared-main/shared-main.module' import { SharedMainModule } from '../shared-main/shared-main.module'
import { SharedModerationModule } from '../shared-moderation' import { SharedModerationModule } from '../shared-moderation'
import { SharedTablesModule } from '../shared-tables'
import { SharedVideoCommentModule } from '../shared-video-comment' import { SharedVideoCommentModule } from '../shared-video-comment'
import { AbuseDetailsComponent } from './abuse-details.component' import { AbuseDetailsComponent } from './abuse-details.component'
import { AbuseListTableComponent } from './abuse-list-table.component' import { AbuseListTableComponent } from './abuse-list-table.component'
import { AbuseMessageModalComponent } from './abuse-message-modal.component' import { AbuseMessageModalComponent } from './abuse-message-modal.component'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component' import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
@NgModule({ @NgModule({
imports: [ imports: [
@ -21,7 +22,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
SharedModerationModule, SharedModerationModule,
SharedGlobalIconModule, SharedGlobalIconModule,
SharedVideoCommentModule, SharedVideoCommentModule,
SharedActorImageModule SharedActorImageModule,
SharedTablesModule
], ],
declarations: [ declarations: [

View File

@ -43,13 +43,8 @@ import {
} from './misc' } from './misc'
import { PluginPlaceholderComponent } from './plugins' import { PluginPlaceholderComponent } from './plugins'
import { ActorRedirectGuard } from './router' import { ActorRedirectGuard } from './router'
import { import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
UserHistoryService, import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
UserNotificationsComponent,
UserNotificationService,
UserQuotaComponent
} from './users'
import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
import { VideoCaptionService } from './video-caption' import { VideoCaptionService } from './video-caption'
import { VideoChannelService } from './video-channel' import { VideoChannelService } from './video-channel'
@ -111,6 +106,8 @@ import { VideoChannelService } from './video-channel'
UserQuotaComponent, UserQuotaComponent,
UserNotificationsComponent, UserNotificationsComponent,
EmbedComponent,
PluginPlaceholderComponent PluginPlaceholderComponent
], ],
@ -167,6 +164,8 @@ import { VideoChannelService } from './video-channel'
UserQuotaComponent, UserQuotaComponent,
UserNotificationsComponent, UserNotificationsComponent,
EmbedComponent,
PluginPlaceholderComponent PluginPlaceholderComponent
], ],

View File

@ -0,0 +1,3 @@
<div class="screenratio">
<div [innerHTML]="embedHTML"></div>
</div>

View File

@ -0,0 +1,10 @@
@use '_mixins' as *;
@use '_variables' as *;
.screenratio {
@include block-ratio($selector: 'div, ::ng-deep iframe') {
width: 100% !important;
height: 100% !important;
left: 0;
};
}

View File

@ -0,0 +1,35 @@
import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment'
import { Component, Input, OnInit } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
import { Video } from '@shared/models'
@Component({
selector: 'my-embed',
styleUrls: [ './embed.component.scss' ],
templateUrl: './embed.component.html'
})
export class EmbedComponent implements OnInit {
@Input() video: Pick<Video, 'name' | 'uuid'>
embedHTML: SafeHtml
constructor (private sanitizer: DomSanitizer) {
}
ngOnInit () {
const html = buildVideoOrPlaylistEmbed(
decorateVideoLink({
url: buildVideoEmbedLink(this.video, environment.originServerUrl),
title: false,
warningTitle: false
}),
this.video.name
)
this.embedHTML = this.sanitizer.bypassSecurityTrustHtml(html)
}
}

View File

@ -1,3 +1,4 @@
export * from './embed.component'
export * from './redundancy.service' export * from './redundancy.service'
export * from './video-details.model' export * from './video-details.model'
export * from './video-edit.model' export * from './video-edit.model'

View File

@ -1,8 +1,9 @@
import { Observable } from 'rxjs' import { SortMeta } from 'primeng/api'
import { catchError, map, switchMap } from 'rxjs/operators' import { from, Observable } from 'rxjs'
import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' import { ComponentPaginationLight, RestExtractor, RestPagination, RestService, ServerService, UserService } from '@app/core'
import { objectToFormData } from '@app/helpers' import { objectToFormData } from '@app/helpers'
import { import {
BooleanBothQuery, BooleanBothQuery,
@ -31,8 +32,8 @@ import { VideoEdit } from './video-edit.model'
import { Video } from './video.model' import { Video } from './video.model'
export type CommonVideoParams = { export type CommonVideoParams = {
videoPagination: ComponentPaginationLight videoPagination?: ComponentPaginationLight
sort: VideoSortField sort: VideoSortField | SortMeta
filter?: VideoFilter filter?: VideoFilter
categoryOneOf?: number[] categoryOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
@ -200,6 +201,31 @@ export class VideoService {
) )
} }
getAdminVideos (
parameters: Omit<CommonVideoParams, 'filter'> & { pagination: RestPagination, search?: string }
): Observable<ResultList<Video>> {
const { pagination, search } = parameters
let params = new HttpParams()
params = this.buildCommonVideosParams({ params, ...parameters })
params = params.set('start', pagination.start.toString())
.set('count', pagination.count.toString())
if (search) {
params = this.buildAdminParamsFromSearch(search, params)
}
if (!params.has('filter')) params = params.set('filter', 'all')
return this.authHttp
.get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
.pipe(
switchMap(res => this.extractVideos(res)),
catchError(err => this.restExtractor.handleError(err))
)
}
getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> { getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> {
let params = new HttpParams() let params = new HttpParams()
params = this.buildCommonVideosParams({ params, ...parameters }) params = this.buildCommonVideosParams({ params, ...parameters })
@ -284,11 +310,13 @@ export class VideoService {
) )
} }
removeVideo (id: number) { removeVideo (idArg: number | number[]) {
return this.authHttp const ids = Array.isArray(idArg) ? idArg : [ idArg ]
.delete(VideoService.BASE_VIDEO_URL + id)
return from(ids)
.pipe( .pipe(
map(this.restExtractor.extractDataBool), concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)),
toArray(),
catchError(err => this.restExtractor.handleError(err)) catchError(err => this.restExtractor.handleError(err))
) )
} }
@ -393,9 +421,23 @@ export class VideoService {
} }
private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) { private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
const { params, videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive, nsfw } = options const {
params,
videoPagination,
sort,
filter,
categoryOneOf,
languageOneOf,
skipCount,
nsfwPolicy,
isLive,
nsfw
} = options
const pagination = videoPagination
? this.restService.componentToRestPagination(videoPagination)
: undefined
const pagination = this.restService.componentToRestPagination(videoPagination)
let newParams = this.restService.addRestGetParams(params, pagination, sort) let newParams = this.restService.addRestGetParams(params, pagination, sort)
if (filter) newParams = newParams.set('filter', filter) if (filter) newParams = newParams.set('filter', filter)
@ -409,4 +451,19 @@ export class VideoService {
return newParams return newParams
} }
private buildAdminParamsFromSearch (search: string, params: HttpParams) {
const filters = this.restService.parseQueryStringFilter(search, {
filter: {
prefix: 'local:',
handler: v => {
if (v === 'true') return 'all-local'
return 'all'
}
}
})
return this.restService.addObjectParams(params, filters)
}
} }

View File

@ -40,14 +40,6 @@
} }
} }
.screenratio {
@include block-ratio($selector: 'div, ::ng-deep iframe') {
width: 100% !important;
height: 100% !important;
left: 0;
};
}
.chip { .chip {
@include chip; @include chip;
} }
@ -58,13 +50,6 @@ my-action-dropdown.show {
} }
} }
.table-video-link {
@include disable-outline;
position: relative;
top: 3px;
}
.table-comment-link, .table-comment-link,
.table-account-link { .table-account-link {
@include disable-outline; @include disable-outline;
@ -81,68 +66,6 @@ my-action-dropdown.show {
flex-direction: column; flex-direction: column;
} }
.table-video {
display: inline-flex;
.table-video-image {
$image-height: 45px;
@include miniature-thumbnail;
@include margin-right(0.5rem);
height: $image-height;
width: #{math.div(16, 9) * $image-height};
border-radius: 2px;
border: 0;
background: transparent;
display: inline-flex;
justify-content: center;
position: relative;
img {
height: 100%;
width: 100%;
border-radius: 2px;
}
span {
color: pvar(--inputPlaceholderColor);
}
.table-video-image-label {
@include static-thumbnail-overlay;
position: absolute;
border-radius: 3px;
font-size: 10px;
padding: 0 3px;
line-height: 1.3;
bottom: 2px;
right: 2px;
}
}
.table-video-text {
display: inline-flex;
flex-direction: column;
justify-content: center;
font-size: 90%;
color: pvar(--mainForegroundColor);
line-height: 1rem;
div .glyphicon {
@include margin-left(0.1rem);
font-size: 80%;
color: #808080;
}
div + div {
color: var(--greyForegroundColor);
font-size: 11px;
}
}
}
my-abuse-details { my-abuse-details {
width: 100%; width: 100%;
} }

View File

@ -19,9 +19,3 @@ textarea {
@include margin-left(10px); @include margin-left(10px);
} }
} }
.screenratio {
@include block-ratio($selector: 'div, ::ng-deep iframe') {
left: 0;
};
}

View File

@ -35,9 +35,7 @@
<div class="col-7"> <div class="col-7">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-lg-9 mb-2"> <div class="col-12 col-lg-9 mb-2">
<div class="screenratio"> <my-embed [video]="video"></my-embed>
<div [innerHTML]="embedHtml"></div>
</div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,11 @@
import { mapValues, pickBy } from 'lodash-es' import { mapValues, pickBy } from 'lodash-es'
import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
import { Component, Input, OnInit, ViewChild } from '@angular/core' import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser' import { DomSanitizer } from '@angular/platform-browser'
import { Notifier } from '@app/core' import { Notifier } from '@app/core'
import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { decorateVideoLink } from '@shared/core-utils'
import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
import { AbusePredefinedReasonsString } from '@shared/models' import { AbusePredefinedReasonsString } from '@shared/models'
import { Video } from '../../shared-main' import { Video } from '../../shared-main'
@ -25,7 +23,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
error: string = null error: string = null
predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
embedHtml: SafeHtml
private openedModal: NgbModalRef private openedModal: NgbModalRef
@ -55,20 +52,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
return this.form.get('timestamp').value return this.form.get('timestamp').value
} }
getVideoEmbed () {
return this.sanitizer.bypassSecurityTrustHtml(
buildVideoOrPlaylistEmbed(
decorateVideoLink({
url: this.video.embedUrl,
title: false,
warningTitle: false
}),
this.video.name
)
)
}
ngOnInit () { ngOnInit () {
this.buildForm({ this.buildForm({
reason: ABUSE_REASON_VALIDATOR, reason: ABUSE_REASON_VALIDATOR,
@ -82,8 +65,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
}) })
this.predefinedReasons = this.abuseService.getPrefefinedReasons('video') this.predefinedReasons = this.abuseService.getPrefefinedReasons('video')
this.embedHtml = this.getVideoEmbed()
} }
show () { show () {

View File

@ -0,0 +1,3 @@
export * from './table-expander-icon.component'
export * from './video-cell.component'
export * from './shared-tables.module'

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core'
import { SharedMainModule } from '../shared-main/shared-main.module'
import { TableExpanderIconComponent } from './table-expander-icon.component'
import { VideoCellComponent } from './video-cell.component'
@NgModule({
imports: [
SharedMainModule
],
declarations: [
VideoCellComponent,
TableExpanderIconComponent
],
exports: [
VideoCellComponent,
TableExpanderIconComponent
],
providers: [
]
})
export class SharedTablesModule { }

View File

@ -0,0 +1,12 @@
import { Component, Input } from '@angular/core'
@Component({
selector: 'my-table-expander-icon',
template: `
<span class="expander">
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span>`
})
export class TableExpanderIconComponent {
@Input() expanded: boolean
}

View File

@ -0,0 +1,19 @@
<a [href]="getVideoUrl()" class="table-video-link" [title]="video.name" target="_blank" rel="noopener noreferrer">
<div class="table-video">
<div class="table-video-image">
<img [src]="video.thumbnailPath">
<ng-content select="[image]"></ng-content>
</div>
<div class="table-video-text">
<div>
<ng-content select="[name]"></ng-content>
{{ video.name }}
</div>
<div class="text-muted">by {{ video.channel?.displayName }} on {{ video.channel?.host }} </div>
</div>
</div>
</a>

View File

@ -0,0 +1,74 @@
@use 'sass:math';
@use '_mixins' as *;
@use '_variables' as *;
@use '_miniature' as *;
.table-video-link {
@include disable-outline;
position: relative;
top: 3px;
}
.table-video {
display: inline-flex;
.table-video-image {
$image-height: 45px;
@include miniature-thumbnail;
@include margin-right(0.5rem);
height: $image-height;
width: #{math.div(16, 9) * $image-height};
border-radius: 2px;
border: 0;
background: transparent;
display: inline-flex;
justify-content: center;
position: relative;
img {
height: 100%;
width: 100%;
border-radius: 2px;
}
span {
color: pvar(--inputPlaceholderColor);
}
.table-video-image-label {
@include static-thumbnail-overlay;
position: absolute;
border-radius: 3px;
font-size: 10px;
padding: 0 3px;
line-height: 1.3;
bottom: 2px;
right: 2px;
}
}
.table-video-text {
display: inline-flex;
flex-direction: column;
justify-content: center;
font-size: 90%;
color: pvar(--mainForegroundColor);
line-height: 1rem;
div .glyphicon {
@include margin-left(0.1rem);
font-size: 80%;
color: #808080;
}
div + div {
color: var(--greyForegroundColor);
font-size: 11px;
}
}
}

View File

@ -0,0 +1,15 @@
import { Component, Input } from '@angular/core'
import { Video } from '@app/shared/shared-main'
@Component({
selector: 'my-video-cell',
styleUrls: [ 'video-cell.component.scss' ],
templateUrl: 'video-cell.component.html'
})
export class VideoCellComponent {
@Input() video: Video
getVideoUrl () {
return Video.buildWatchUrl(this.video)
}
}