]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add videos list admin component
authorChocobozzz <me@florianbigard.com>
Wed, 27 Oct 2021 09:42:05 +0000 (11:42 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 29 Oct 2021 09:48:21 +0000 (11:48 +0200)
32 files changed:
client/src/app/+admin/admin.component.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
client/src/app/+admin/overview/index.ts
client/src/app/+admin/overview/overview.routes.ts
client/src/app/+admin/overview/videos/index.ts [new file with mode: 0644]
client/src/app/+admin/overview/videos/video-list.component.html [new file with mode: 0644]
client/src/app/+admin/overview/videos/video-list.component.scss [new file with mode: 0644]
client/src/app/+admin/overview/videos/video-list.component.ts [new file with mode: 0644]
client/src/app/+admin/overview/videos/video.routes.ts [new file with mode: 0644]
client/src/app/shared/shared-abuse-list/abuse-details.component.html
client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
client/src/app/shared/shared-abuse-list/processed-abuse.model.ts
client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
client/src/app/shared/shared-main/shared-main.module.ts
client/src/app/shared/shared-main/video/embed.component.html [new file with mode: 0644]
client/src/app/shared/shared-main/video/embed.component.scss [new file with mode: 0644]
client/src/app/shared/shared-main/video/embed.component.ts [new file with mode: 0644]
client/src/app/shared/shared-main/video/index.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/app/shared/shared-moderation/moderation.scss
client/src/app/shared/shared-moderation/report-modals/report.component.scss
client/src/app/shared/shared-moderation/report-modals/video-report.component.html
client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
client/src/app/shared/shared-tables/index.ts [new file with mode: 0644]
client/src/app/shared/shared-tables/shared-tables.module.ts [new file with mode: 0644]
client/src/app/shared/shared-tables/table-expander-icon.component.ts [new file with mode: 0644]
client/src/app/shared/shared-tables/video-cell.component.html [new file with mode: 0644]
client/src/app/shared/shared-tables/video-cell.component.scss [new file with mode: 0644]
client/src/app/shared/shared-tables/video-cell.component.ts [new file with mode: 0644]

index 27d5e0a101b7b9b333920d5e23aa332d3ea46169..b8a957d1c451cccf3468e630debdc7ef6a9f2391 100644 (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) {
       this.menuEntries.push(overviewItems)
     }
@@ -217,4 +225,8 @@ export class AdminComponent implements OnInit {
   private hasVideoCommentsRight () {
     return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS)
   }
+
+  private hasVideosRight () {
+    return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
+  }
 }
index a2bd8888073b7371cf45a9cafd6048e23d23dde7..d04c11a2050ff001939b5fa78c2dfa2a11bd0ef8 100644 (file)
@@ -10,7 +10,9 @@ import { SharedFormModule } from '@app/shared/shared-forms'
 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 {
@@ -33,7 +35,7 @@ import { AbuseListComponent, VideoBlockListComponent } from './moderation'
 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'
@@ -56,6 +58,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
     SharedActorImageModule,
     SharedActorImageEditModule,
     SharedCustomMarkupModule,
+    SharedVideoMiniatureModule,
+    SharedTablesModule,
 
     TableModule,
     SelectButtonModule,
@@ -65,6 +69,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
   declarations: [
     AdminComponent,
 
+    VideoListComponent,
+
     FollowsComponent,
     FollowersListComponent,
     FollowingListComponent,
index 7efa87dd0b067bffedf93ecb80339879c3bfb2f3..3cd69cfbcd853f99a010ac12362e7f8dd342476b 100644 (file)
@@ -34,9 +34,7 @@
     <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>
@@ -90,9 +77,7 @@
           </div>
 
           <div class="right">
-            <div class="screenratio">
-              <div [innerHTML]="videoBlock.embedHtml"></div>
-            </div>
+            <my-embed [video]="videoBlock.video"></my-embed>
           </div>
 
         </div>
index 7baf34ca24a509f3f032873cba27cbe0e06a7804..1fe8d0f9d38c5dac3fa19b5b1b3d11136923229f 100644 (file)
@@ -3,11 +3,10 @@ import { switchMap } from 'rxjs/operators'
 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'
@@ -18,7 +17,7 @@ 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 }
@@ -50,7 +49,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
     private confirmService: ConfirmService,
     private videoBlocklistService: VideoBlockService,
     private markdownRenderer: MarkdownService,
-    private sanitizer: DomSanitizer,
     private videoService: VideoService
   ) {
     super()
@@ -125,10 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
     return 'VideoBlockListComponent'
   }
 
-  getVideoUrl (videoBlock: VideoBlacklist) {
-    return Video.buildWatchUrl(videoBlock.video)
-  }
-
   toHtml (text: string) {
     return this.markdownRenderer.textMarkdownToHTML(text)
   }
@@ -176,8 +170,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
 
           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)
             })
           }
         },
index b71a6a45fc456d1a40718361063403fd6d4d077b..a9c46893f345828feda8601dd7b964114ed9aaae 100644 (file)
@@ -1,2 +1,3 @@
 export * from './users'
+export * from './videos'
 export * from './overview.routes'
index cb59860723d4102705aebda3a1a966d9eb7df2ef..1e6686d16a197e9bdb55b0df68d2151a640c117a 100644 (file)
@@ -1,6 +1,8 @@
 import { Routes } from '@angular/router'
 import { UsersRoutes } from './users'
+import { VideosRoutes } from './videos'
 
 export const OverviewRoutes: Routes = [
-  ...UsersRoutes
+  ...UsersRoutes,
+  ...VideosRoutes
 ]
diff --git a/client/src/app/+admin/overview/videos/index.ts b/client/src/app/+admin/overview/videos/index.ts
new file mode 100644 (file)
index 0000000..40c2ffe
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './video-list.component'
+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 (file)
index 0000000..1f1e9cc
--- /dev/null
@@ -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>
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 (file)
index 0000000..fcdb457
--- /dev/null
@@ -0,0 +1,10 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+my-embed {
+  display: block;
+  max-width: 500px;
+}
+
+.badge {
+  @include table-badge;
+}
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 (file)
index 0000000..a445bc2
--- /dev/null
@@ -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)
+      })
+  }
+}
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 (file)
index 0000000..984df7b
--- /dev/null
@@ -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`
+          }
+        }
+      }
+    ]
+  }
+]
index 2f0bc5ae5eb54b821cef6d14df8a932f6a57d250..a1a4586f082605c7b9a8cbe7751bac3490906a62 100644 (file)
@@ -85,9 +85,9 @@
 
   <!-- 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">
index 4bf83316bd5e6cc14c2024267671ea2f95cbbec4..d957eaeab332a4760f0a904446707cb967190b50 100644 (file)
@@ -30,9 +30,7 @@
   <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">
index 297993e39cb47c038e6e95cd7d1e0543f92bfc5c..10f5861b9f0e264cb48d19bc38d85627141275e2 100644 (file)
@@ -1,8 +1,6 @@
 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'
@@ -10,7 +8,6 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable }
 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'
@@ -133,19 +130,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
     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
@@ -220,8 +204,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
           }
 
           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)
             }
index 194d52a333dab782c809259197609217ddb3a564..b9a9bd889dd7e47674e56656b1c3eb62e4d2c655 100644 (file)
@@ -1,13 +1,11 @@
-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
index 8f3830a17211d9ff6cbe651106fed0b0cae94452..eeda27fa679a6941fb840825c874969f59f610bc 100644 (file)
@@ -1,16 +1,17 @@
 
 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: [
@@ -21,7 +22,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
     SharedModerationModule,
     SharedGlobalIconModule,
     SharedVideoCommentModule,
-    SharedActorImageModule
+    SharedActorImageModule,
+    SharedTablesModule
   ],
 
   declarations: [
index 93989780d447938ea81abc8ea34f5d91af589207..a90b59e417647da86c1bbbfe01b18b1301499081 100644 (file)
@@ -43,13 +43,8 @@ import {
 } 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'
 
@@ -111,6 +106,8 @@ import { VideoChannelService } from './video-channel'
     UserQuotaComponent,
     UserNotificationsComponent,
 
+    EmbedComponent,
+
     PluginPlaceholderComponent
   ],
 
@@ -167,6 +164,8 @@ import { VideoChannelService } from './video-channel'
     UserQuotaComponent,
     UserNotificationsComponent,
 
+    EmbedComponent,
+
     PluginPlaceholderComponent
   ],
 
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 (file)
index 0000000..3b088d0
--- /dev/null
@@ -0,0 +1,3 @@
+<div class="screenratio">
+  <div [innerHTML]="embedHTML"></div>
+</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 (file)
index 0000000..420ba6f
--- /dev/null
@@ -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;
+  };
+}
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 (file)
index 0000000..4732efa
--- /dev/null
@@ -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)
+  }
+}
index 3053df4ef539f0747e58b0068eed68419b68f10c..e72c0c3d6757a444a09ccaaebed6500f871a8078 100644 (file)
@@ -1,3 +1,4 @@
+export * from './embed.component'
 export * from './redundancy.service'
 export * from './video-details.model'
 export * from './video-edit.model'
index 7935569e7902de5c3b2ff04c98b976cafb57f912..9e3aa1e6a0093847d7c9be53838ce3d3ecf0bc91 100644 (file)
@@ -1,8 +1,9 @@
-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,
@@ -31,8 +32,8 @@ import { VideoEdit } from './video-edit.model'
 import { Video } from './video.model'
 
 export type CommonVideoParams = {
-  videoPagination: ComponentPaginationLight
-  sort: VideoSortField
+  videoPagination?: ComponentPaginationLight
+  sort: VideoSortField | SortMeta
   filter?: VideoFilter
   categoryOneOf?: number[]
   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>> {
     let params = new HttpParams()
     params = this.buildCommonVideosParams({ params, ...parameters })
@@ -284,13 +310,15 @@ export class VideoService {
                )
   }
 
-  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) {
@@ -393,9 +421,23 @@ export class VideoService {
   }
 
   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)
@@ -409,4 +451,19 @@ export class VideoService {
 
     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)
+  }
 }
index 815e2791f7f7cf2cb0729642cf0cdd79814c65be..eaf5a825026b378e05f27b84e414eb0f4de92ad4 100644 (file)
   }
 }
 
-.screenratio {
-  @include block-ratio($selector: 'div, ::ng-deep iframe') {
-    width: 100% !important;
-    height: 100% !important;
-    left: 0;
-  };
-}
-
 .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-account-link {
   @include disable-outline;
@@ -81,68 +66,6 @@ my-action-dropdown.show {
   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%;
 }
index 06e50ac2d657e2851f43a38eaf8c729c5cb03d68..76ec0a6ed1c2717ab8c1b4f12ce1420c5c816d80 100644 (file)
@@ -19,9 +19,3 @@ textarea {
     @include margin-left(10px);
   }
 }
-
-.screenratio {
-  @include block-ratio($selector: 'div, ::ng-deep iframe') {
-    left: 0;
-  };
-}
index 1aae64bff571ad9806de08d55a94de80acdd1adb..afac108fc63f99a11b821394642bc20597af60c0 100644 (file)
@@ -35,9 +35,7 @@
       <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>
 
index 278d60ac66461c805f24ef69d0a283d9a7862d70..38dd929108496cedc2e0c12a92f9aff392a62150 100644 (file)
@@ -1,13 +1,11 @@
 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'
@@ -25,7 +23,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
 
   error: string = null
   predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
-  embedHtml: SafeHtml
 
   private openedModal: NgbModalRef
 
@@ -55,20 +52,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
     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,
@@ -82,8 +65,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
     })
 
     this.predefinedReasons = this.abuseService.getPrefefinedReasons('video')
-
-    this.embedHtml = this.getVideoEmbed()
   }
 
   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 (file)
index 0000000..e7b5932
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './table-expander-icon.component'
+export * from './video-cell.component'
+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 (file)
index 0000000..c528365
--- /dev/null
@@ -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 { }
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 (file)
index 0000000..3756b47
--- /dev/null
@@ -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
+}
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 (file)
index 0000000..fb7d852
--- /dev/null
@@ -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>
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 (file)
index 0000000..7efb615
--- /dev/null
@@ -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;
+    }
+  }
+}
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 (file)
index 0000000..6298418
--- /dev/null
@@ -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)
+  }
+}