})
}
+ if (this.hasVideosRight()) {
+ overviewItems.children.push({
+ label: $localize`Videos`,
+ routerLink: '/admin/videos',
+ iconName: 'videos'
+ })
+ }
+
if (overviewItems.children.length !== 0) {
this.menuEntries.push(overviewItems)
}
private hasVideoCommentsRight () {
return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS)
}
+
+ private hasVideosRight () {
+ return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
+ }
}
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
import { SharedModerationModule } from '@app/shared/shared-moderation'
+import { SharedTablesModule } from '@app/shared/shared-tables'
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
+import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
import { AdminRoutingModule } from './admin-routing.module'
import { AdminComponent } from './admin.component'
import {
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
import { ModerationComponent } from './moderation/moderation.component'
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 { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
SharedActorImageModule,
SharedActorImageEditModule,
SharedCustomMarkupModule,
+ SharedVideoMiniatureModule,
+ SharedTablesModule,
TableModule,
SelectButtonModule,
declarations: [
AdminComponent,
+ VideoListComponent,
+
FollowsComponent,
FollowersListComponent,
FollowingListComponent,
<tr>
<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">
- <span class="expander">
- <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
- </span>
+ <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
</td>
<td class="action-cell">
</td>
<td>
- <a [href]="getVideoUrl(videoBlock)" class="table-video-link" [title]="videoBlock.video.name" target="_blank" rel="noopener noreferrer">
- <div class="table-video">
- <div class="table-video-image">
- <img [src]="videoBlock.video.thumbnailPath">
- </div>
-
- <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>
+ <my-video-cell [video]="videoBlock.video">
+ <span name>
+ <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>
+ </span>
+ </my-video-cell>
</td>
<td>
</div>
<div class="right">
- <div class="screenratio">
- <div [innerHTML]="videoBlock.embedHtml"></div>
- </div>
+ <my-embed [video]="videoBlock.video"></my-embed>
</div>
</div>
import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment'
import { Component, OnInit } from '@angular/core'
-import { DomSanitizer } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
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 { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ]
})
export class VideoBlockListComponent extends RestTable implements OnInit {
- blocklist: (VideoBlacklist & { reasonHtml?: string, embedHtml?: string })[] = []
+ blocklist: (VideoBlacklist & { reasonHtml?: string })[] = []
totalRecords = 0
sort: SortMeta = { field: 'createdAt', order: -1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
private confirmService: ConfirmService,
private videoBlocklistService: VideoBlockService,
private markdownRenderer: MarkdownService,
- private sanitizer: DomSanitizer,
private videoService: VideoService
) {
super()
return 'VideoBlockListComponent'
}
- getVideoUrl (videoBlock: VideoBlacklist) {
- return Video.buildWatchUrl(videoBlock.video)
- }
-
toHtml (text: string) {
return this.markdownRenderer.textMarkdownToHTML(text)
}
for (const element of this.blocklist) {
Object.assign(element, {
- reasonHtml: await this.toHtml(element.reason),
- embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(element))
+ reasonHtml: await this.toHtml(element.reason)
})
}
},
export * from './users'
+export * from './videos'
export * from './overview.routes'
import { Routes } from '@angular/router'
import { UsersRoutes } from './users'
+import { VideosRoutes } from './videos'
export const OverviewRoutes: Routes = [
- ...UsersRoutes
+ ...UsersRoutes,
+ ...VideosRoutes
]
--- /dev/null
+export * from './video-list.component'
+export * from './video.routes'
--- /dev/null
+<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>
--- /dev/null
+@use '_variables' as *;
+@use '_mixins' as *;
+my-embed {
+ display: block;
+ max-width: 500px;
+}
+
+.badge {
+ @include table-badge;
+}
--- /dev/null
+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)
+ })
+ }
+}
--- /dev/null
+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`
+ }
+ }
+ }
+ ]
+ }
+]
<!-- report right part (video/comment details) -->
<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" [innerHTML]="abuse.embedHtml"></div>
+ <my-embed *ngIf="!abuse.video.deleted" [video]="abuse.video"></my-embed>
</div>
<div *ngIf="abuse.comment" class="comment-html">
<ng-template pTemplate="body" let-expanded="expanded" let-abuse>
<tr>
<td class="expand-cell c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
- <span class="expander">
- <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
- </span>
+ <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
</td>
<td class="action-cell">
<ng-container *ngIf="abuse.video">
<td *ngIf="!abuse.video.deleted">
- <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
- <div class="table-video">
- <div class="table-video-image">
- <img [src]="abuse.video.thumbnailPath">
- <span
- class="table-video-image-label" *ngIf="abuse.count > 1"
- i18n-title title="This video has been reported multiple times."
- >
- {{ abuse.nth }}/{{ abuse.count }}
- </span>
- </div>
-
- <div class="table-video-text">
- <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>
- {{ abuse.video.name }}
- </div>
- <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
- </div>
- </div>
- </a>
+ <my-video-cell [video]="abuse.video">
+ <span image>
+ <span
+ class="table-video-image-label" *ngIf="abuse.count > 1"
+ i18n-title title="This video has been reported multiple times."
+ >
+ {{ abuse.nth }}/{{ abuse.count }}
+ </span>
+ </span>
+
+ <span name>
+ <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
+ </span>
+ </my-video-cell>
</td>
<td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
import * as debug from 'debug'
import truncate from 'lodash-es/truncate'
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 { DomSanitizer } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
import { VideoCommentService } from '@app/shared/shared-video-comment'
-import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
import { AbuseState, AdminAbuse } from '@shared/models'
import { AdvancedInputFilter } from '../shared-forms'
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
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) {
const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`)
if (res === false) return
}
if (abuse.video) {
- abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
-
if (abuse.video.channel?.ownerAccount) {
abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
}
-import { SafeHtml } from '@angular/platform-browser'
-import { AdminAbuse } from '@shared/models'
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
// And this model is only used in this component
export type ProcessedAbuse = AdminAbuse & {
moderationCommentHtml?: string
reasonHtml?: string
- embedHtml?: SafeHtml
updatedAt?: Date
// override bare server-side definitions with rich client-side definitions
import { TableModule } from 'primeng/table'
import { NgModule } from '@angular/core'
+import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
import { SharedFormModule } from '../shared-forms/shared-form.module'
import { SharedGlobalIconModule } from '../shared-icons'
import { SharedMainModule } from '../shared-main/shared-main.module'
import { SharedModerationModule } from '../shared-moderation'
+import { SharedTablesModule } from '../shared-tables'
import { SharedVideoCommentModule } from '../shared-video-comment'
import { AbuseDetailsComponent } from './abuse-details.component'
import { AbuseListTableComponent } from './abuse-list-table.component'
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
-import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
@NgModule({
imports: [
SharedModerationModule,
SharedGlobalIconModule,
SharedVideoCommentModule,
- SharedActorImageModule
+ SharedActorImageModule,
+ SharedTablesModule
],
declarations: [
} from './misc'
import { PluginPlaceholderComponent } from './plugins'
import { ActorRedirectGuard } from './router'
-import {
- UserHistoryService,
- UserNotificationsComponent,
- UserNotificationService,
- UserQuotaComponent
-} from './users'
-import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
+import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
+import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
import { VideoCaptionService } from './video-caption'
import { VideoChannelService } from './video-channel'
UserQuotaComponent,
UserNotificationsComponent,
+ EmbedComponent,
+
PluginPlaceholderComponent
],
UserQuotaComponent,
UserNotificationsComponent,
+ EmbedComponent,
+
PluginPlaceholderComponent
],
--- /dev/null
+<div class="screenratio">
+ <div [innerHTML]="embedHTML"></div>
+</div>
--- /dev/null
+@use '_mixins' as *;
+@use '_variables' as *;
+
+.screenratio {
+ @include block-ratio($selector: 'div, ::ng-deep iframe') {
+ width: 100% !important;
+ height: 100% !important;
+ left: 0;
+ };
+}
--- /dev/null
+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)
+ }
+}
+export * from './embed.component'
export * from './redundancy.service'
export * from './video-details.model'
export * from './video-edit.model'
-import { Observable } from 'rxjs'
-import { catchError, map, switchMap } from 'rxjs/operators'
+import { SortMeta } from 'primeng/api'
+import { from, Observable } from 'rxjs'
+import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
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 {
BooleanBothQuery,
import { Video } from './video.model'
export type CommonVideoParams = {
- videoPagination: ComponentPaginationLight
- sort: VideoSortField
+ videoPagination?: ComponentPaginationLight
+ sort: VideoSortField | SortMeta
filter?: VideoFilter
categoryOneOf?: number[]
languageOneOf?: string[]
)
}
+ 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>> {
let params = new HttpParams()
params = this.buildCommonVideosParams({ params, ...parameters })
)
}
- removeVideo (id: number) {
- return this.authHttp
- .delete(VideoService.BASE_VIDEO_URL + id)
- .pipe(
- map(this.restExtractor.extractDataBool),
- catchError(err => this.restExtractor.handleError(err))
- )
+ removeVideo (idArg: number | number[]) {
+ const ids = Array.isArray(idArg) ? idArg : [ idArg ]
+
+ return from(ids)
+ .pipe(
+ concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)),
+ toArray(),
+ catchError(err => this.restExtractor.handleError(err))
+ )
}
loadCompleteDescription (descriptionPath: string) {
}
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)
if (filter) newParams = newParams.set('filter', filter)
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)
+ }
}
}
}
-.screenratio {
- @include block-ratio($selector: 'div, ::ng-deep iframe') {
- width: 100% !important;
- height: 100% !important;
- left: 0;
- };
-}
-
.chip {
@include chip;
}
}
}
-.table-video-link {
- @include disable-outline;
-
- position: relative;
- top: 3px;
-}
-
.table-comment-link,
.table-account-link {
@include disable-outline;
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 {
width: 100%;
}
@include margin-left(10px);
}
}
-
-.screenratio {
- @include block-ratio($selector: 'div, ::ng-deep iframe') {
- left: 0;
- };
-}
<div class="col-7">
<div class="row justify-content-center">
<div class="col-12 col-lg-9 mb-2">
- <div class="screenratio">
- <div [innerHTML]="embedHtml"></div>
- </div>
+ <my-embed [video]="video"></my-embed>
</div>
</div>
import { mapValues, pickBy } from 'lodash-es'
-import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
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 { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
-import { decorateVideoLink } from '@shared/core-utils'
import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
import { AbusePredefinedReasonsString } from '@shared/models'
import { Video } from '../../shared-main'
error: string = null
predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
- embedHtml: SafeHtml
private openedModal: NgbModalRef
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 () {
this.buildForm({
reason: ABUSE_REASON_VALIDATOR,
})
this.predefinedReasons = this.abuseService.getPrefefinedReasons('video')
-
- this.embedHtml = this.getVideoEmbed()
}
show () {
--- /dev/null
+export * from './table-expander-icon.component'
+export * from './video-cell.component'
+export * from './shared-tables.module'
--- /dev/null
+
+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 { }
--- /dev/null
+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
+}
--- /dev/null
+<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>
--- /dev/null
+@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;
+ }
+ }
+}
--- /dev/null
+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)
+ }
+}