diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-08-13 15:07:23 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-11-25 11:07:56 +0100 |
commit | afff310e50f2fa8419bb4242470cbde46ab54463 (patch) | |
tree | 34efda2daf8f7cdfd89ef6616a79e2222082f93a /client | |
parent | f619de0e435f7ac3abad2ec772397486358b56e7 (diff) | |
download | PeerTube-afff310e50f2fa8419bb4242470cbde46ab54463.tar.gz PeerTube-afff310e50f2fa8419bb4242470cbde46ab54463.tar.zst PeerTube-afff310e50f2fa8419bb4242470cbde46ab54463.zip |
allow private syndication feeds via a user feedToken
Diffstat (limited to 'client')
19 files changed, 263 insertions, 26 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 09539fa92..e73a9f8a8 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -243,7 +243,7 @@ | |||
243 | <div class="form-row mt-5"> <!-- appearance grid --> | 243 | <div class="form-row mt-5"> <!-- appearance grid --> |
244 | <div class="form-group col-12 col-lg-4 col-xl-3"> | 244 | <div class="form-group col-12 col-lg-4 col-xl-3"> |
245 | <div i18n class="inner-form-title">APPEARANCE</div> | 245 | <div i18n class="inner-form-title">APPEARANCE</div> |
246 | <div i18n class="inner-for-description"> | 246 | <div i18n class="inner-form-description"> |
247 | Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="customizations" (click)="gotoAnchor()">add slight customizations</a>. | 247 | Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="customizations" (click)="gotoAnchor()">add slight customizations</a>. |
248 | </div> | 248 | </div> |
249 | </div> | 249 | </div> |
diff --git a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts index e5dd723ff..9316fc0dd 100644 --- a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts +++ b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | |||
2 | import { Component } from '@angular/core' | 1 | import { Component } from '@angular/core' |
3 | 2 | ||
4 | @Component({ | 3 | @Component({ |
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html new file mode 100644 index 000000000..62e2cb59b --- /dev/null +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html | |||
@@ -0,0 +1,35 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>Applications</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <div class="form-row"> <!-- built-in token grid --> | ||
7 | <div class="form-group col-12 col-lg-4 col-xl-3"> | ||
8 | <h2 i18n class="applications-title">SUBSCRIPTION FEED</h2> | ||
9 | <div i18n class="applications-description"> | ||
10 | Used to retrieve the list of videos of the creators | ||
11 | you subscribed to from outside PeerTube | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | ||
16 | |||
17 | <div class="form-group"> | ||
18 | <label i18n for="feed-url">Feed URL</label> | ||
19 | <my-input-readonly-copy [value]="feedUrl"></my-input-readonly-copy> | ||
20 | </div> | ||
21 | |||
22 | <div class="form-group"> | ||
23 | <label i18n for="feed-token">Feed Token</label> | ||
24 | <my-input-readonly-copy [value]="feedToken"></my-input-readonly-copy> | ||
25 | </div> | ||
26 | |||
27 | </div> | ||
28 | </div> | ||
29 | |||
30 | <div class="form-row mt-4"> <!-- submit placement block --> | ||
31 | <div class="col-md-7 col-xl-5"></div> | ||
32 | <div class="col-md-5 col-xl-5"> | ||
33 | <input (click)="renewToken()" type="submit" i18n-value value="Renew token"> | ||
34 | </div> | ||
35 | </div> | ||
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss b/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss new file mode 100644 index 000000000..704132c03 --- /dev/null +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss | |||
@@ -0,0 +1,28 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | label { | ||
5 | font-weight: $font-regular; | ||
6 | font-size: 100%; | ||
7 | } | ||
8 | |||
9 | .applications-title { | ||
10 | @include settings-big-title; | ||
11 | } | ||
12 | |||
13 | .form-group { | ||
14 | max-width: 500px; | ||
15 | } | ||
16 | |||
17 | input[type=submit] { | ||
18 | @include peertube-button; | ||
19 | @include orange-button; | ||
20 | |||
21 | display: flex; | ||
22 | margin-left: auto; | ||
23 | |||
24 | & + .form-error { | ||
25 | display: inline; | ||
26 | margin-left: 5px; | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts b/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts new file mode 100644 index 000000000..c3f09dfe3 --- /dev/null +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | |||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { AuthService, Notifier, ConfirmService } from '@app/core' | ||
4 | import { VideoService } from '@app/shared/shared-main' | ||
5 | import { FeedFormat } from '@shared/models' | ||
6 | import { Subject, merge } from 'rxjs' | ||
7 | import { debounceTime } from 'rxjs/operators' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-account-applications', | ||
11 | templateUrl: './my-account-applications.component.html', | ||
12 | styleUrls: [ './my-account-applications.component.scss' ] | ||
13 | }) | ||
14 | export class MyAccountApplicationsComponent implements OnInit { | ||
15 | feedUrl: string | ||
16 | feedToken: string | ||
17 | |||
18 | private baseURL = window.location.protocol + '//' + window.location.host | ||
19 | private tokenStream = new Subject() | ||
20 | |||
21 | constructor ( | ||
22 | private authService: AuthService, | ||
23 | private videoService: VideoService, | ||
24 | private notifier: Notifier, | ||
25 | private confirmService: ConfirmService | ||
26 | ) {} | ||
27 | |||
28 | ngOnInit () { | ||
29 | this.feedUrl = this.baseURL | ||
30 | |||
31 | merge( | ||
32 | this.tokenStream, | ||
33 | this.authService.userInformationLoaded | ||
34 | ).pipe(debounceTime(400)) | ||
35 | .subscribe( | ||
36 | _ => { | ||
37 | const user = this.authService.getUser() | ||
38 | this.videoService.getVideoSubscriptionFeedUrls(user.account.id) | ||
39 | .then(feeds => this.feedUrl = this.baseURL + feeds.find(f => f.format === FeedFormat.RSS).url) | ||
40 | .then(_ => this.authService.getScopedTokens().then(tokens => this.feedToken = tokens.feedToken)) | ||
41 | }, | ||
42 | |||
43 | err => { | ||
44 | this.notifier.error(err.message) | ||
45 | } | ||
46 | ) | ||
47 | } | ||
48 | |||
49 | async renewToken () { | ||
50 | const res = await this.confirmService.confirm('Renewing the token will disallow previously configured clients from retrieving the feed until they use the new token. Proceed?', 'Renew token') | ||
51 | if (res === false) return | ||
52 | |||
53 | await this.authService.renewScopedTokens() | ||
54 | this.notifier.success('Token renewed. Update your client configuration accordingly.') | ||
55 | this.tokenStream.next() | ||
56 | } | ||
57 | } | ||
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 81380ec6e..226a4a7be 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts | |||
@@ -8,6 +8,7 @@ import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-acc | |||
8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
10 | import { MyAccountComponent } from './my-account.component' | 10 | import { MyAccountComponent } from './my-account.component' |
11 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
11 | 12 | ||
12 | const myAccountRoutes: Routes = [ | 13 | const myAccountRoutes: Routes = [ |
13 | { | 14 | { |
@@ -117,6 +118,15 @@ const myAccountRoutes: Routes = [ | |||
117 | title: $localize`My abuse reports` | 118 | title: $localize`My abuse reports` |
118 | } | 119 | } |
119 | } | 120 | } |
121 | }, | ||
122 | { | ||
123 | path: 'applications', | ||
124 | component: MyAccountApplicationsComponent, | ||
125 | data: { | ||
126 | meta: { | ||
127 | title: 'Applications' | ||
128 | } | ||
129 | } | ||
120 | } | 130 | } |
121 | ] | 131 | ] |
122 | } | 132 | } |
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index d6e9d1c15..12966aebb 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts | |||
@@ -41,6 +41,11 @@ export class MyAccountComponent implements OnInit { | |||
41 | label: $localize`Abuse reports`, | 41 | label: $localize`Abuse reports`, |
42 | routerLink: '/my-account/abuses', | 42 | routerLink: '/my-account/abuses', |
43 | iconName: 'flag' | 43 | iconName: 'flag' |
44 | }, | ||
45 | { | ||
46 | label: $localize`Applications`, | ||
47 | routerLink: '/my-account/applications', | ||
48 | iconName: 'codesandbox' | ||
44 | } | 49 | } |
45 | ] | 50 | ] |
46 | } | 51 | } |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 9e3fbcf65..70bf58aae 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -21,6 +21,7 @@ import { MyAccountNotificationPreferencesComponent } from './my-account-settings | |||
21 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' | 21 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' |
22 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 22 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
23 | import { MyAccountComponent } from './my-account.component' | 23 | import { MyAccountComponent } from './my-account.component' |
24 | import { VideoChangeOwnershipComponent } from './my-account-applications/my-account-applications.component' | ||
24 | 25 | ||
25 | @NgModule({ | 26 | @NgModule({ |
26 | imports: [ | 27 | imports: [ |
@@ -51,6 +52,7 @@ import { MyAccountComponent } from './my-account.component' | |||
51 | MyAccountAbusesListComponent, | 52 | MyAccountAbusesListComponent, |
52 | MyAccountServerBlocklistComponent, | 53 | MyAccountServerBlocklistComponent, |
53 | MyAccountNotificationsComponent, | 54 | MyAccountNotificationsComponent, |
55 | MyAccountNotificationPreferencesComponent, | ||
54 | MyAccountNotificationPreferencesComponent | 56 | MyAccountNotificationPreferencesComponent |
55 | ], | 57 | ], |
56 | 58 | ||
diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts index 6988c574b..10031d6cc 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts | |||
@@ -3,9 +3,12 @@ import { ActivatedRoute, Router } from '@angular/router' | |||
3 | import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' | 3 | import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' |
4 | import { HooksService } from '@app/core/plugins/hooks.service' | 4 | import { HooksService } from '@app/core/plugins/hooks.service' |
5 | import { immutableAssign } from '@app/helpers' | 5 | import { immutableAssign } from '@app/helpers' |
6 | import { VideoService } from '@app/shared/shared-main' | ||
6 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | 7 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' |
7 | import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' | 8 | import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' |
8 | import { VideoSortField } from '@shared/models' | 9 | import { VideoSortField, FeedFormat } from '@shared/models' |
10 | import { copyToClipboard } from '../../../root-helpers/utils' | ||
11 | import { environment } from '../../../environments/environment' | ||
9 | 12 | ||
10 | @Component({ | 13 | @Component({ |
11 | selector: 'my-videos-user-subscriptions', | 14 | selector: 'my-videos-user-subscriptions', |
@@ -28,11 +31,13 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement | |||
28 | protected screenService: ScreenService, | 31 | protected screenService: ScreenService, |
29 | protected storageService: LocalStorageService, | 32 | protected storageService: LocalStorageService, |
30 | private userSubscription: UserSubscriptionService, | 33 | private userSubscription: UserSubscriptionService, |
31 | private hooks: HooksService | 34 | private hooks: HooksService, |
35 | private videoService: VideoService | ||
32 | ) { | 36 | ) { |
33 | super() | 37 | super() |
34 | 38 | ||
35 | this.titlePage = $localize`Videos from your subscriptions` | 39 | this.titlePage = $localize`Videos from your subscriptions` |
40 | |||
36 | this.actions.push({ | 41 | this.actions.push({ |
37 | routerLink: '/my-library/subscriptions', | 42 | routerLink: '/my-library/subscriptions', |
38 | label: $localize`Subscriptions`, | 43 | label: $localize`Subscriptions`, |
@@ -42,6 +47,20 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement | |||
42 | 47 | ||
43 | ngOnInit () { | 48 | ngOnInit () { |
44 | super.ngOnInit() | 49 | super.ngOnInit() |
50 | |||
51 | const user = this.authService.getUser() | ||
52 | let feedUrl = environment.embedUrl | ||
53 | this.videoService.getVideoSubscriptionFeedUrls(user.account.id) | ||
54 | .then((feeds: any) => feedUrl = feedUrl + feeds.find((f: any) => f.format === FeedFormat.RSS).url) | ||
55 | this.actions.unshift({ | ||
56 | label: $localize`Feed`, | ||
57 | iconName: 'syndication', | ||
58 | justIcon: true, | ||
59 | click: () => { | ||
60 | copyToClipboard(feedUrl) | ||
61 | this.activateCopiedMessage() | ||
62 | } | ||
63 | }) | ||
45 | } | 64 | } |
46 | 65 | ||
47 | ngOnDestroy () { | 66 | ngOnDestroy () { |
@@ -68,4 +87,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement | |||
68 | generateSyndicationList () { | 87 | generateSyndicationList () { |
69 | // not implemented yet | 88 | // not implemented yet |
70 | } | 89 | } |
90 | |||
91 | activateCopiedMessage () { | ||
92 | this.notifier.success($localize`Feed URL copied`) | ||
93 | } | ||
71 | } | 94 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index fd6062d3f..224f35f82 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -11,6 +11,7 @@ import { environment } from '../../../environments/environment' | |||
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
12 | import { AuthStatus } from './auth-status.model' | 12 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 13 | import { AuthUser } from './auth-user.model' |
14 | import { ScopedTokenType, ScopedToken } from '@shared/models/users/user-scoped-token' | ||
14 | 15 | ||
15 | interface UserLoginWithUsername extends UserLogin { | 16 | interface UserLoginWithUsername extends UserLogin { |
16 | access_token: string | 17 | access_token: string |
@@ -26,6 +27,7 @@ export class AuthService { | |||
26 | private static BASE_CLIENT_URL = environment.apiUrl + '/api/v1/oauth-clients/local' | 27 | private static BASE_CLIENT_URL = environment.apiUrl + '/api/v1/oauth-clients/local' |
27 | private static BASE_TOKEN_URL = environment.apiUrl + '/api/v1/users/token' | 28 | private static BASE_TOKEN_URL = environment.apiUrl + '/api/v1/users/token' |
28 | private static BASE_REVOKE_TOKEN_URL = environment.apiUrl + '/api/v1/users/revoke-token' | 29 | private static BASE_REVOKE_TOKEN_URL = environment.apiUrl + '/api/v1/users/revoke-token' |
30 | private static BASE_SCOPED_TOKENS_URL = environment.apiUrl + '/api/v1/users/scoped-tokens' | ||
29 | private static BASE_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me' | 31 | private static BASE_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me' |
30 | private static LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { | 32 | private static LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { |
31 | CLIENT_ID: 'client_id', | 33 | CLIENT_ID: 'client_id', |
@@ -41,6 +43,7 @@ export class AuthService { | |||
41 | private loginChanged: Subject<AuthStatus> | 43 | private loginChanged: Subject<AuthStatus> |
42 | private user: AuthUser = null | 44 | private user: AuthUser = null |
43 | private refreshingTokenObservable: Observable<any> | 45 | private refreshingTokenObservable: Observable<any> |
46 | private scopedTokens: ScopedToken | ||
44 | 47 | ||
45 | constructor ( | 48 | constructor ( |
46 | private http: HttpClient, | 49 | private http: HttpClient, |
@@ -244,6 +247,48 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular | |||
244 | ) | 247 | ) |
245 | } | 248 | } |
246 | 249 | ||
250 | getScopedTokens (): Promise<ScopedToken> { | ||
251 | return new Promise((res, rej) => { | ||
252 | if (this.scopedTokens) return res(this.scopedTokens) | ||
253 | |||
254 | const authHeaderValue = this.getRequestHeaderValue() | ||
255 | const headers = new HttpHeaders().set('Authorization', authHeaderValue) | ||
256 | |||
257 | this.http.get<ScopedToken>(AuthService.BASE_SCOPED_TOKENS_URL, { headers }) | ||
258 | .subscribe( | ||
259 | scopedTokens => { | ||
260 | this.scopedTokens = scopedTokens | ||
261 | res(this.scopedTokens) | ||
262 | }, | ||
263 | |||
264 | err => { | ||
265 | console.error(err) | ||
266 | rej(err) | ||
267 | } | ||
268 | ) | ||
269 | }) | ||
270 | } | ||
271 | |||
272 | renewScopedTokens (): Promise<ScopedToken> { | ||
273 | return new Promise((res, rej) => { | ||
274 | const authHeaderValue = this.getRequestHeaderValue() | ||
275 | const headers = new HttpHeaders().set('Authorization', authHeaderValue) | ||
276 | |||
277 | this.http.post<ScopedToken>(AuthService.BASE_SCOPED_TOKENS_URL, {}, { headers }) | ||
278 | .subscribe( | ||
279 | scopedTokens => { | ||
280 | this.scopedTokens = scopedTokens | ||
281 | res(this.scopedTokens) | ||
282 | }, | ||
283 | |||
284 | err => { | ||
285 | console.error(err) | ||
286 | rej(err) | ||
287 | } | ||
288 | ) | ||
289 | }) | ||
290 | } | ||
291 | |||
247 | private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> { | 292 | private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> { |
248 | // User is not loaded yet, set manually auth header | 293 | // User is not loaded yet, set manually auth header |
249 | const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) | 294 | const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) |
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index f3c1fe59b..53a2aee9a 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts | |||
@@ -69,7 +69,8 @@ const icons = { | |||
69 | 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default, | 69 | 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default, |
70 | 'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default, | 70 | 'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default, |
71 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, | 71 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, |
72 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default | 72 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, |
73 | 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default | ||
73 | } | 74 | } |
74 | 75 | ||
75 | export type GlobalIconName = keyof typeof icons | 76 | export type GlobalIconName = keyof typeof icons |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index c8a3ec043..b81540e8d 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -2,7 +2,7 @@ import { Observable } from 'rxjs' | |||
2 | import { catchError, map, switchMap } from 'rxjs/operators' | 2 | import { catchError, map, switchMap } from 'rxjs/operators' |
3 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | 3 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' | 5 | import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' |
6 | import { objectToFormData } from '@app/helpers' | 6 | import { objectToFormData } from '@app/helpers' |
7 | import { | 7 | import { |
8 | FeedFormat, | 8 | FeedFormat, |
@@ -49,7 +49,8 @@ export class VideoService implements VideosProvider { | |||
49 | private authHttp: HttpClient, | 49 | private authHttp: HttpClient, |
50 | private restExtractor: RestExtractor, | 50 | private restExtractor: RestExtractor, |
51 | private restService: RestService, | 51 | private restService: RestService, |
52 | private serverService: ServerService | 52 | private serverService: ServerService, |
53 | private authService: AuthService | ||
53 | ) {} | 54 | ) {} |
54 | 55 | ||
55 | getVideoViewUrl (uuid: string) { | 56 | getVideoViewUrl (uuid: string) { |
@@ -293,6 +294,16 @@ export class VideoService implements VideosProvider { | |||
293 | return this.buildBaseFeedUrls(params) | 294 | return this.buildBaseFeedUrls(params) |
294 | } | 295 | } |
295 | 296 | ||
297 | async getVideoSubscriptionFeedUrls (accountId: number) { | ||
298 | let params = this.restService.addRestGetParams(new HttpParams()) | ||
299 | params = params.set('accountId', accountId.toString()) | ||
300 | |||
301 | const { feedToken } = await this.authService.getScopedTokens() | ||
302 | params = params.set('token', feedToken) | ||
303 | |||
304 | return this.buildBaseFeedUrls(params) | ||
305 | } | ||
306 | |||
296 | getVideoFileMetadata (metadataUrl: string) { | 307 | getVideoFileMetadata (metadataUrl: string) { |
297 | return this.authHttp | 308 | return this.authHttp |
298 | .get<VideoFileMetadata>(metadataUrl) | 309 | .get<VideoFileMetadata>(metadataUrl) |
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html index b1ac757db..18294513f 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html | |||
@@ -8,9 +8,25 @@ | |||
8 | 8 | ||
9 | <div class="action-block"> | 9 | <div class="action-block"> |
10 | <my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed> | 10 | <my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed> |
11 | <a [routerLink]="action.routerLink" routerLinkActive="active" *ngFor="let action of actions"> | 11 | <ng-container *ngFor="let action of actions"> |
12 | <my-button [icon]="action.iconName" [label]="action.label"></my-button> | 12 | <a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active"> |
13 | </a> | 13 | <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> |
14 | </a> | ||
15 | <a *ngIf="!action.routerLink && action.click && !action.clipboard" class="ml-2" (click)="action.click()" (key.enter)="action.click()"> | ||
16 | <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> | ||
17 | </a> | ||
18 | <a *ngIf="!action.routerLink && !action.click && action.clipboard" class="ml-2" [cdkCopyToClipboard]="action.clipboard"> | ||
19 | <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> | ||
20 | </a> | ||
21 | <a *ngIf="!action.routerLink && action.click && action.clipboard" class="ml-2" (click)="action.click()" (key.enter)="action.click()" [cdkCopyToClipboard]="action.clipboard"> | ||
22 | <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> | ||
23 | </a> | ||
24 | |||
25 | <ng-template #actionContent let-action> | ||
26 | <my-button *ngIf="!action.justIcon" [icon]="action.iconName" [label]="action.label"></my-button> | ||
27 | <my-button *ngIf="action.justIcon" [icon]="action.iconName" [ngbTooltip]="action.label"></my-button> | ||
28 | </ng-template> | ||
29 | </ng-container> | ||
14 | </div> | 30 | </div> |
15 | 31 | ||
16 | <div class="moderation-block" *ngIf="displayModerationBlock"> | 32 | <div class="moderation-block" *ngIf="displayModerationBlock"> |
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts index 2219ced30..c55e85afe 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts | |||
@@ -70,9 +70,12 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
70 | } | 70 | } |
71 | 71 | ||
72 | actions: { | 72 | actions: { |
73 | routerLink: string | ||
74 | iconName: GlobalIconName | 73 | iconName: GlobalIconName |
75 | label: string | 74 | label: string |
75 | justIcon?: boolean | ||
76 | routerLink?: string | ||
77 | click?: Function | ||
78 | clipboard?: string | ||
76 | }[] = [] | 79 | }[] = [] |
77 | 80 | ||
78 | onDataSubject = new Subject<any[]>() | 81 | onDataSubject = new Subject<any[]>() |
diff --git a/client/src/assets/images/feather/codesandbox.svg b/client/src/assets/images/feather/codesandbox.svg new file mode 100644 index 000000000..49848f520 --- /dev/null +++ b/client/src/assets/images/feather/codesandbox.svg | |||
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codesandbox"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline><polyline points="7.5 19.79 7.5 14.6 3 12"></polyline><polyline points="21 12 16.5 14.6 16.5 19.79"></polyline><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg> \ No newline at end of file | |||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index da23c59a7..9407cf123 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -35,7 +35,8 @@ import { | |||
35 | VideoJSPluginOptions | 35 | VideoJSPluginOptions |
36 | } from './peertube-videojs-typings' | 36 | } from './peertube-videojs-typings' |
37 | import { TranslationsManager } from './translations-manager' | 37 | import { TranslationsManager } from './translations-manager' |
38 | import { buildVideoOrPlaylistEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isSafari, isIOS } from './utils' | 38 | import { buildVideoOrPlaylistEmbed, buildVideoLink, getRtcConfig, isSafari, isIOS } from './utils' |
39 | import { copyToClipboard } from '../../root-helpers/utils' | ||
39 | 40 | ||
40 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | 41 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) |
41 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' | 42 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' |
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index ce7a7fe6c..280f721bd 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -176,18 +176,6 @@ function buildVideoOrPlaylistEmbed (embedUrl: string) { | |||
176 | '</iframe>' | 176 | '</iframe>' |
177 | } | 177 | } |
178 | 178 | ||
179 | function copyToClipboard (text: string) { | ||
180 | const el = document.createElement('textarea') | ||
181 | el.value = text | ||
182 | el.setAttribute('readonly', '') | ||
183 | el.style.position = 'absolute' | ||
184 | el.style.left = '-9999px' | ||
185 | document.body.appendChild(el) | ||
186 | el.select() | ||
187 | document.execCommand('copy') | ||
188 | document.body.removeChild(el) | ||
189 | } | ||
190 | |||
191 | function videoFileMaxByResolution (files: VideoFile[]) { | 179 | function videoFileMaxByResolution (files: VideoFile[]) { |
192 | let max = files[0] | 180 | let max = files[0] |
193 | 181 | ||
@@ -236,7 +224,6 @@ export { | |||
236 | buildVideoOrPlaylistEmbed, | 224 | buildVideoOrPlaylistEmbed, |
237 | videoFileMaxByResolution, | 225 | videoFileMaxByResolution, |
238 | videoFileMinByResolution, | 226 | videoFileMinByResolution, |
239 | copyToClipboard, | ||
240 | isMobile, | 227 | isMobile, |
241 | bytes, | 228 | bytes, |
242 | isIOS, | 229 | isIOS, |
diff --git a/client/src/root-helpers/utils.ts b/client/src/root-helpers/utils.ts index de4e08bf5..e32187ddb 100644 --- a/client/src/root-helpers/utils.ts +++ b/client/src/root-helpers/utils.ts | |||
@@ -9,6 +9,18 @@ function objectToUrlEncoded (obj: any) { | |||
9 | return str.join('&') | 9 | return str.join('&') |
10 | } | 10 | } |
11 | 11 | ||
12 | function copyToClipboard (text: string) { | ||
13 | const el = document.createElement('textarea') | ||
14 | el.value = text | ||
15 | el.setAttribute('readonly', '') | ||
16 | el.style.position = 'absolute' | ||
17 | el.style.left = '-9999px' | ||
18 | document.body.appendChild(el) | ||
19 | el.select() | ||
20 | document.execCommand('copy') | ||
21 | document.body.removeChild(el) | ||
22 | } | ||
23 | |||
12 | // Thanks: https://github.com/uupaa/dynamic-import-polyfill | 24 | // Thanks: https://github.com/uupaa/dynamic-import-polyfill |
13 | function importModule (path: string) { | 25 | function importModule (path: string) { |
14 | return new Promise((resolve, reject) => { | 26 | return new Promise((resolve, reject) => { |
@@ -51,6 +63,7 @@ function wait (ms: number) { | |||
51 | } | 63 | } |
52 | 64 | ||
53 | export { | 65 | export { |
66 | copyToClipboard, | ||
54 | importModule, | 67 | importModule, |
55 | objectToUrlEncoded, | 68 | objectToUrlEncoded, |
56 | wait | 69 | wait |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index e6491b492..4d70110fe 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -225,7 +225,7 @@ | |||
225 | line-height: $button-height; | 225 | line-height: $button-height; |
226 | border-radius: 3px; | 226 | border-radius: 3px; |
227 | text-align: center; | 227 | text-align: center; |
228 | padding: 0 17px 0 13px; | 228 | padding: 0 13px 0 13px; |
229 | cursor: pointer; | 229 | cursor: pointer; |
230 | } | 230 | } |
231 | 231 | ||