aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/buttons/button.component.scss23
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config-validators.service.ts2
-rw-r--r--client/src/app/shared/images/global-icon.component.ts1
-rw-r--r--client/src/app/shared/instance/instance-statistics.component.ts23
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.ts3
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts4
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/shared/video/infinite-scroller.directive.ts4
-rw-r--r--client/src/app/shared/video/redundancy.service.ts73
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.ts28
-rw-r--r--client/src/app/shared/video/video-miniature.component.html2
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts3
-rw-r--r--client/src/app/shared/video/video.model.ts19
13 files changed, 153 insertions, 34 deletions
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss
index 2a8cfc748..3ccfefd7e 100644
--- a/client/src/app/shared/buttons/button.component.scss
+++ b/client/src/app/shared/buttons/button.component.scss
@@ -10,11 +10,26 @@ my-small-loader ::ng-deep .root {
10.action-button { 10.action-button {
11 @include peertube-button-link; 11 @include peertube-button-link;
12 @include button-with-icon(21px, 0, -2px); 12 @include button-with-icon(21px, 0, -2px);
13}
13 14
14 // FIXME: Firefox does not apply global .orange-button icon color 15.orange-button {
15 &.orange-button { 16 @include peertube-button;
16 @include apply-svg-color(#fff) 17 @include orange-button;
17 } 18}
19
20.orange-button-link {
21 @include peertube-button-link;
22 @include orange-button;
23}
24
25.grey-button {
26 @include peertube-button;
27 @include grey-button;
28}
29
30.grey-button-link {
31 @include peertube-button-link;
32 @include grey-button;
18} 33}
19 34
20// In a table, try to minimize the space taken by this button 35// In a table, try to minimize the space taken by this button
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
index 767e3f026..d20754d11 100644
--- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
@@ -56,7 +56,7 @@ export class CustomConfigValidatorsService {
56 } 56 }
57 57
58 this.SIGNUP_LIMIT = { 58 this.SIGNUP_LIMIT = {
59 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], 59 VALIDATORS: [ Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+') ],
60 MESSAGES: { 60 MESSAGES: {
61 'required': this.i18n('Signup limit is required.'), 61 'required': this.i18n('Signup limit is required.'),
62 'min': this.i18n('Signup limit must be greater than 1.'), 62 'min': this.i18n('Signup limit must be greater than 1.'),
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts
index 806aca347..b6e641228 100644
--- a/client/src/app/shared/images/global-icon.component.ts
+++ b/client/src/app/shared/images/global-icon.component.ts
@@ -1,6 +1,5 @@
1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' 1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core'
2import { HooksService } from '@app/core/plugins/hooks.service' 2import { HooksService } from '@app/core/plugins/hooks.service'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4 3
5const icons = { 4const icons = {
6 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'), 5 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'),
diff --git a/client/src/app/shared/instance/instance-statistics.component.ts b/client/src/app/shared/instance/instance-statistics.component.ts
index 8ec728f05..40aa8a4c0 100644
--- a/client/src/app/shared/instance/instance-statistics.component.ts
+++ b/client/src/app/shared/instance/instance-statistics.component.ts
@@ -1,9 +1,6 @@
1import { map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { ServerStats } from '@shared/models/server' 2import { ServerStats } from '@shared/models/server'
6import { environment } from '../../../environments/environment' 3import { ServerService } from '@app/core'
7 4
8@Component({ 5@Component({
9 selector: 'my-instance-statistics', 6 selector: 'my-instance-statistics',
@@ -11,27 +8,15 @@ import { environment } from '../../../environments/environment'
11 styleUrls: [ './instance-statistics.component.scss' ] 8 styleUrls: [ './instance-statistics.component.scss' ]
12}) 9})
13export class InstanceStatisticsComponent implements OnInit { 10export class InstanceStatisticsComponent implements OnInit {
14 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
15
16 serverStats: ServerStats = null 11 serverStats: ServerStats = null
17 12
18 constructor ( 13 constructor (
19 private http: HttpClient, 14 private serverService: ServerService
20 private i18n: I18n
21 ) { 15 ) {
22 } 16 }
23 17
24 ngOnInit () { 18 ngOnInit () {
25 this.getStats() 19 this.serverService.getServerStats()
26 .subscribe( 20 .subscribe(res => this.serverStats = res)
27 res => {
28 this.serverStats = res
29 }
30 )
31 }
32
33 getStats () {
34 return this.http
35 .get<ServerStats>(InstanceStatisticsComponent.BASE_STATS_URL)
36 } 21 }
37} 22}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
index 5ccdafb54..24a083654 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.ts
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -49,8 +49,7 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
49 e => e.children && e.children.some(c => !!c.iconName) 49 e => e.children && e.children.some(c => !!c.iconName)
50 ) 50 )
51 51
52 // FIXME: We have to set body for the container to avoid because of scroll overflow on mobile view 52 // We have to set body for the container to avoid scroll overflow on mobile view
53 // But this break our hovering system
54 if (this.screen.isInMobileView()) { 53 if (this.screen.isInMobileView()) {
55 this.container = 'body' 54 this.container = 'body'
56 } 55 }
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index 0d3fde537..f0c87326f 100644
--- a/client/src/app/shared/renderer/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { MarkdownIt } from 'markdown-it'
3import { buildVideoLink } from '../../../assets/player/utils' 2import { buildVideoLink } from '../../../assets/player/utils'
4import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' 3import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service'
4import * as MarkdownIt from 'markdown-it'
5 5
6type MarkdownParsers = { 6type MarkdownParsers = {
7 textMarkdownIt: MarkdownIt 7 textMarkdownIt: MarkdownIt
@@ -100,7 +100,7 @@ export class MarkdownService {
100 } 100 }
101 101
102 private async createMarkdownIt (config: MarkdownConfig) { 102 private async createMarkdownIt (config: MarkdownConfig) {
103 // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function 103 // FIXME: import('...') returns a struct module, containing a "default" field
104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default 104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
105 105
106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) 106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b2eb13f73..d06d37d8c 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -98,6 +98,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
98import { MultiSelectModule } from 'primeng/multiselect' 98import { MultiSelectModule } from 'primeng/multiselect'
99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' 99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' 100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
101import { RedundancyService } from '@app/shared/video/redundancy.service'
101 102
102@NgModule({ 103@NgModule({
103 imports: [ 104 imports: [
@@ -300,6 +301,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
300 UserNotificationService, 301 UserNotificationService,
301 302
302 FollowService, 303 FollowService,
304 RedundancyService,
303 305
304 I18n 306 I18n
305 ] 307 ]
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts
index 9f613c5fa..f09c3d1fc 100644
--- a/client/src/app/shared/video/infinite-scroller.directive.ts
+++ b/client/src/app/shared/video/infinite-scroller.directive.ts
@@ -1,4 +1,4 @@
1import { distinctUntilChanged, filter, map, share, startWith, tap, throttleTime } from 'rxjs/operators' 1import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' 2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
3import { fromEvent, Observable, Subscription } from 'rxjs' 3import { fromEvent, Observable, Subscription } from 'rxjs'
4 4
@@ -53,7 +53,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterConten
53 const scrollableElement = this.onItself ? this.container : window 53 const scrollableElement = this.onItself ? this.container : window
54 const scrollObservable = fromEvent(scrollableElement, 'scroll') 54 const scrollObservable = fromEvent(scrollableElement, 'scroll')
55 .pipe( 55 .pipe(
56 startWith(null as string), // FIXME: typings 56 startWith(true),
57 throttleTime(200, undefined, throttleOptions), 57 throttleTime(200, undefined, throttleOptions),
58 map(() => this.getScrollInfo()), 58 map(() => this.getScrollInfo()),
59 distinctUntilChanged((o1, o2) => o1.current === o2.current), 59 distinctUntilChanged((o1, o2) => o1.current === o2.current),
diff --git a/client/src/app/shared/video/redundancy.service.ts b/client/src/app/shared/video/redundancy.service.ts
new file mode 100644
index 000000000..fb918d73b
--- /dev/null
+++ b/client/src/app/shared/video/redundancy.service.ts
@@ -0,0 +1,73 @@
1import { catchError, map, toArray } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor, RestPagination, RestService } from '@app/shared/rest'
5import { SortMeta } from 'primeng/api'
6import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
7import { concat, Observable } from 'rxjs'
8import { environment } from '../../../environments/environment'
9
10@Injectable()
11export class RedundancyService {
12 static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) { }
19
20 updateRedundancy (host: string, redundancyAllowed: boolean) {
21 const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host
22
23 const body = { redundancyAllowed }
24
25 return this.authHttp.put(url, body)
26 .pipe(
27 map(this.restExtractor.extractDataBool),
28 catchError(err => this.restExtractor.handleError(err))
29 )
30 }
31
32 listVideoRedundancies (options: {
33 pagination: RestPagination,
34 sort: SortMeta,
35 target?: VideoRedundanciesTarget
36 }): Observable<ResultList<VideoRedundancy>> {
37 const { pagination, sort, target } = options
38
39 let params = new HttpParams()
40 params = this.restService.addRestGetParams(params, pagination, sort)
41
42 if (target) params = params.append('target', target)
43
44 return this.authHttp.get<ResultList<VideoRedundancy>>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params })
45 .pipe(
46 catchError(res => this.restExtractor.handleError(res))
47 )
48 }
49
50 addVideoRedundancy (video: Video) {
51 return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id })
52 .pipe(
53 catchError(res => this.restExtractor.handleError(res))
54 )
55 }
56
57 removeVideoRedundancies (redundancy: VideoRedundancy) {
58 const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id)
59 .concat(redundancy.redundancies.files.map(r => r.id))
60 .map(id => this.removeRedundancy(id))
61
62 return concat(...observables)
63 .pipe(toArray())
64 }
65
66 private removeRedundancy (redundancyId: number) {
67 return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId)
68 .pipe(
69 map(this.restExtractor.extractDataBool),
70 catchError(res => this.restExtractor.handleError(res))
71 )
72 }
73}
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts
index afdeab18d..390d74c52 100644
--- a/client/src/app/shared/video/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/video/video-actions-dropdown.component.ts
@@ -14,6 +14,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis
14import { VideoBlacklistService } from '@app/shared/video-blacklist' 14import { VideoBlacklistService } from '@app/shared/video-blacklist'
15import { ScreenService } from '@app/shared/misc/screen.service' 15import { ScreenService } from '@app/shared/misc/screen.service'
16import { VideoCaption } from '@shared/models' 16import { VideoCaption } from '@shared/models'
17import { RedundancyService } from '@app/shared/video/redundancy.service'
17 18
18export type VideoActionsDisplayType = { 19export type VideoActionsDisplayType = {
19 playlist?: boolean 20 playlist?: boolean
@@ -22,6 +23,7 @@ export type VideoActionsDisplayType = {
22 blacklist?: boolean 23 blacklist?: boolean
23 delete?: boolean 24 delete?: boolean
24 report?: boolean 25 report?: boolean
26 duplicate?: boolean
25} 27}
26 28
27@Component({ 29@Component({
@@ -46,7 +48,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
46 update: true, 48 update: true,
47 blacklist: true, 49 blacklist: true,
48 delete: true, 50 delete: true,
49 report: true 51 report: true,
52 duplicate: true
50 } 53 }
51 @Input() placement = 'left' 54 @Input() placement = 'left'
52 55
@@ -74,6 +77,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
74 private screenService: ScreenService, 77 private screenService: ScreenService,
75 private videoService: VideoService, 78 private videoService: VideoService,
76 private blocklistService: BlocklistService, 79 private blocklistService: BlocklistService,
80 private redundancyService: RedundancyService,
77 private i18n: I18n 81 private i18n: I18n
78 ) { } 82 ) { }
79 83
@@ -144,6 +148,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
144 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled 148 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled
145 } 149 }
146 150
151 canVideoBeDuplicated () {
152 return this.video.canBeDuplicatedBy(this.user)
153 }
154
147 /* Action handlers */ 155 /* Action handlers */
148 156
149 async unblacklistVideo () { 157 async unblacklistVideo () {
@@ -186,6 +194,18 @@ export class VideoActionsDropdownComponent implements OnChanges {
186 ) 194 )
187 } 195 }
188 196
197 duplicateVideo () {
198 this.redundancyService.addVideoRedundancy(this.video)
199 .subscribe(
200 () => {
201 const message = this.i18n('This video will be duplicated by your instance.')
202 this.notifier.success(message)
203 },
204
205 err => this.notifier.error(err.message)
206 )
207 }
208
189 onVideoBlacklisted () { 209 onVideoBlacklisted () {
190 this.videoBlacklisted.emit() 210 this.videoBlacklisted.emit()
191 } 211 }
@@ -234,6 +254,12 @@ export class VideoActionsDropdownComponent implements OnChanges {
234 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() 254 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable()
235 }, 255 },
236 { 256 {
257 label: this.i18n('Duplicate (redundancy)'),
258 handler: () => this.duplicateVideo(),
259 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(),
260 iconName: 'cloud-download'
261 },
262 {
237 label: this.i18n('Delete'), 263 label: this.i18n('Delete'),
238 handler: () => this.removeVideo(), 264 handler: () => this.removeVideo(),
239 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), 265 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(),
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html
index 46c49c15b..819be6d48 100644
--- a/client/src/app/shared/video/video-miniature.component.html
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -50,7 +50,7 @@
50 </div> 50 </div>
51 51
52 <div class="video-actions"> 52 <div class="video-actions">
53 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown --> 53 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown: https://github.com/ng-bootstrap/ng-bootstrap/issues/3495 -->
54 <my-video-actions-dropdown 54 <my-video-actions-dropdown
55 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left" 55 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left"
56 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()" 56 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()"
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index 598a7a983..1dfb3eec7 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -64,7 +64,8 @@ export class VideoMiniatureComponent implements OnInit {
64 update: true, 64 update: true,
65 blacklist: true, 65 blacklist: true,
66 delete: true, 66 delete: true,
67 report: true 67 report: true,
68 duplicate: false
68 } 69 }
69 showActions = false 70 showActions = false
70 serverConfig: ServerConfig 71 serverConfig: ServerConfig
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index fb98d5382..546518cca 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -42,6 +42,9 @@ export class Video implements VideoServerModel {
42 dislikes: number 42 dislikes: number
43 nsfw: boolean 43 nsfw: boolean
44 44
45 originInstanceUrl: string
46 originInstanceHost: string
47
45 waitTranscoding?: boolean 48 waitTranscoding?: boolean
46 state?: VideoConstant<VideoState> 49 state?: VideoConstant<VideoState>
47 scheduledUpdate?: VideoScheduleUpdate 50 scheduledUpdate?: VideoScheduleUpdate
@@ -86,22 +89,31 @@ export class Video implements VideoServerModel {
86 this.waitTranscoding = hash.waitTranscoding 89 this.waitTranscoding = hash.waitTranscoding
87 this.state = hash.state 90 this.state = hash.state
88 this.description = hash.description 91 this.description = hash.description
92
89 this.duration = hash.duration 93 this.duration = hash.duration
90 this.durationLabel = durationToString(hash.duration) 94 this.durationLabel = durationToString(hash.duration)
95
91 this.id = hash.id 96 this.id = hash.id
92 this.uuid = hash.uuid 97 this.uuid = hash.uuid
98
93 this.isLocal = hash.isLocal 99 this.isLocal = hash.isLocal
94 this.name = hash.name 100 this.name = hash.name
101
95 this.thumbnailPath = hash.thumbnailPath 102 this.thumbnailPath = hash.thumbnailPath
96 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath 103 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
104
97 this.previewPath = hash.previewPath 105 this.previewPath = hash.previewPath
98 this.previewUrl = absoluteAPIUrl + hash.previewPath 106 this.previewUrl = absoluteAPIUrl + hash.previewPath
107
99 this.embedPath = hash.embedPath 108 this.embedPath = hash.embedPath
100 this.embedUrl = absoluteAPIUrl + hash.embedPath 109 this.embedUrl = absoluteAPIUrl + hash.embedPath
110
101 this.views = hash.views 111 this.views = hash.views
102 this.likes = hash.likes 112 this.likes = hash.likes
103 this.dislikes = hash.dislikes 113 this.dislikes = hash.dislikes
114
104 this.nsfw = hash.nsfw 115 this.nsfw = hash.nsfw
116
105 this.account = hash.account 117 this.account = hash.account
106 this.channel = hash.channel 118 this.channel = hash.channel
107 119
@@ -124,6 +136,9 @@ export class Video implements VideoServerModel {
124 this.blacklistedReason = hash.blacklistedReason 136 this.blacklistedReason = hash.blacklistedReason
125 137
126 this.userHistory = hash.userHistory 138 this.userHistory = hash.userHistory
139
140 this.originInstanceHost = this.account.host
141 this.originInstanceUrl = 'https://' + this.originInstanceHost
127 } 142 }
128 143
129 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 144 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
@@ -152,4 +167,8 @@ export class Video implements VideoServerModel {
152 isUpdatableBy (user: AuthUser) { 167 isUpdatableBy (user: AuthUser) {
153 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) 168 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
154 } 169 }
170
171 canBeDuplicatedBy (user: AuthUser) {
172 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES)
173 }
155} 174}