diff options
Diffstat (limited to 'client')
158 files changed, 2929 insertions, 1140 deletions
diff --git a/client/e2e/src/po/my-account.ts b/client/e2e/src/po/my-account.ts new file mode 100644 index 000000000..e49372983 --- /dev/null +++ b/client/e2e/src/po/my-account.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import { by, element } from 'protractor' | ||
2 | |||
3 | export class MyAccountPage { | ||
4 | |||
5 | navigateToMyVideos () { | ||
6 | return element(by.css('a[href="/my-account/videos"]')).click() | ||
7 | } | ||
8 | |||
9 | navigateToMyPlaylists () { | ||
10 | return element(by.css('a[href="/my-account/video-playlists"]')).click() | ||
11 | } | ||
12 | |||
13 | navigateToMyHistory () { | ||
14 | return element(by.css('a[href="/my-account/history/videos"]')).click() | ||
15 | } | ||
16 | |||
17 | // My account Videos | ||
18 | |||
19 | getLastVideoName () { | ||
20 | return this.getAllVideoNameElements().first().getText() | ||
21 | } | ||
22 | |||
23 | removeLastVideo () { | ||
24 | return this.getLastVideoElement().element(by.css('my-delete-button')).click() | ||
25 | } | ||
26 | |||
27 | validRemove () { | ||
28 | return element(by.css('.action-button-submit')).click() | ||
29 | } | ||
30 | |||
31 | countVideos () { | ||
32 | return this.getAllVideoNameElements().count() | ||
33 | } | ||
34 | |||
35 | // My account playlists | ||
36 | |||
37 | getLastUpdatedPlaylistName () { | ||
38 | return this.getLastUpdatedPlaylist().element(by.css('.miniature-name')).getText() | ||
39 | } | ||
40 | |||
41 | getLastUpdatedPlaylistVideosText () { | ||
42 | return this.getLastUpdatedPlaylist().element(by.css('.miniature-playlist-info-overlay')).getText() | ||
43 | } | ||
44 | |||
45 | clickOnLastUpdatedPlaylist () { | ||
46 | return this.getLastUpdatedPlaylist().element(by.css('.miniature-thumbnail')).click() | ||
47 | } | ||
48 | |||
49 | countTotalPlaylistElements () { | ||
50 | return element.all(by.css('my-video-playlist-element-miniature')).count() | ||
51 | } | ||
52 | |||
53 | playPlaylist () { | ||
54 | return element(by.css('.playlist-info .miniature-thumbnail')).click() | ||
55 | } | ||
56 | |||
57 | // My account Videos | ||
58 | |||
59 | private getLastVideoElement () { | ||
60 | return element.all(by.css('.video')).first() | ||
61 | } | ||
62 | |||
63 | private getAllVideoNameElements () { | ||
64 | return element.all(by.css('.video-miniature-name')) | ||
65 | } | ||
66 | |||
67 | // My account playlists | ||
68 | |||
69 | private getLastUpdatedPlaylist () { | ||
70 | return element.all(by.css('my-video-playlist-miniature')).first() | ||
71 | } | ||
72 | } | ||
diff --git a/client/e2e/src/po/video-update.po.ts b/client/e2e/src/po/video-update.po.ts new file mode 100644 index 000000000..4de3b1b1d --- /dev/null +++ b/client/e2e/src/po/video-update.po.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { by, element } from 'protractor' | ||
2 | |||
3 | export class VideoUpdatePage { | ||
4 | |||
5 | async updateName (videoName: string) { | ||
6 | const nameInput = element(by.css('input#name')) | ||
7 | await nameInput.clear() | ||
8 | await nameInput.sendKeys(videoName) | ||
9 | } | ||
10 | |||
11 | async validUpdate () { | ||
12 | const submitButton = await this.getSubmitButton() | ||
13 | |||
14 | return submitButton.click() | ||
15 | } | ||
16 | |||
17 | private getSubmitButton () { | ||
18 | return element(by.css('.submit-button:not(.disabled) input')) | ||
19 | } | ||
20 | } | ||
diff --git a/client/e2e/src/po/video-watch.po.ts b/client/e2e/src/po/video-watch.po.ts index 5f61d5668..9bb0a3919 100644 --- a/client/e2e/src/po/video-watch.po.ts +++ b/client/e2e/src/po/video-watch.po.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { browser, by, element } from 'protractor' | 1 | import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor' |
2 | 2 | ||
3 | export class VideoWatchPage { | 3 | export class VideoWatchPage { |
4 | async goOnVideosList (isMobileDevice: boolean, isSafari: boolean) { | 4 | async goOnVideosList (isMobileDevice: boolean, isSafari: boolean) { |
@@ -44,6 +44,10 @@ export class VideoWatchPage { | |||
44 | .then(seconds => parseInt(seconds, 10)) | 44 | .then(seconds => parseInt(seconds, 10)) |
45 | } | 45 | } |
46 | 46 | ||
47 | getVideoName () { | ||
48 | return this.getVideoNameElement().getText() | ||
49 | } | ||
50 | |||
47 | async playAndPauseVideo (isAutoplay: boolean, isMobileDevice: boolean) { | 51 | async playAndPauseVideo (isAutoplay: boolean, isMobileDevice: boolean) { |
48 | if (isAutoplay === false) { | 52 | if (isAutoplay === false) { |
49 | const playButton = element(by.css('.vjs-big-play-button')) | 53 | const playButton = element(by.css('.vjs-big-play-button')) |
@@ -101,4 +105,41 @@ export class VideoWatchPage { | |||
101 | async goOnP2PMediaLoaderEmbed () { | 105 | async goOnP2PMediaLoaderEmbed () { |
102 | return browser.get('https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50?mode=p2p-media-loader') | 106 | return browser.get('https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50?mode=p2p-media-loader') |
103 | } | 107 | } |
108 | |||
109 | async clickOnUpdate () { | ||
110 | const dropdown = element(by.css('my-video-actions-dropdown .action-button')) | ||
111 | await dropdown.click() | ||
112 | |||
113 | const items: ElementFinder[] = await element.all(by.css('my-video-actions-dropdown .dropdown-menu .dropdown-item')) | ||
114 | |||
115 | for (const item of items) { | ||
116 | const href = await item.getAttribute('href') | ||
117 | |||
118 | if (href && href.includes('/update/')) { | ||
119 | await item.click() | ||
120 | return | ||
121 | } | ||
122 | } | ||
123 | } | ||
124 | |||
125 | async clickOnSave () { | ||
126 | return element(by.css('.action-button-save')).click() | ||
127 | } | ||
128 | |||
129 | async saveToWatchLater () { | ||
130 | return element.all(by.css('my-video-add-to-playlist .playlist')).first().click() | ||
131 | } | ||
132 | |||
133 | waitUntilVideoName (name: string, maxTime: number) { | ||
134 | const elem = this.getVideoNameElement() | ||
135 | |||
136 | return browser.wait(ExpectedConditions.textToBePresentInElement(elem, name), maxTime) | ||
137 | } | ||
138 | |||
139 | private getVideoNameElement () { | ||
140 | // We have 2 video info name block, pick the first that is not empty | ||
141 | return element.all(by.css('.video-bottom .video-info-name')) | ||
142 | .filter(e => e.getText().then(t => !!t)) | ||
143 | .first() | ||
144 | } | ||
104 | } | 145 | } |
diff --git a/client/e2e/src/videos.e2e-spec.ts b/client/e2e/src/videos.e2e-spec.ts index 25521cad9..c19ab3092 100644 --- a/client/e2e/src/videos.e2e-spec.ts +++ b/client/e2e/src/videos.e2e-spec.ts | |||
@@ -2,35 +2,56 @@ import { VideoWatchPage } from './po/video-watch.po' | |||
2 | import { VideoUploadPage } from './po/video-upload.po' | 2 | import { VideoUploadPage } from './po/video-upload.po' |
3 | import { LoginPage } from './po/login.po' | 3 | import { LoginPage } from './po/login.po' |
4 | import { browser } from 'protractor' | 4 | import { browser } from 'protractor' |
5 | import { VideoUpdatePage } from './po/video-update.po' | ||
6 | import { MyAccountPage } from './po/my-account' | ||
7 | |||
8 | async function skipIfUploadNotSupported () { | ||
9 | if (await isMobileDevice() || await isSafari()) { | ||
10 | console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') | ||
11 | return true | ||
12 | } | ||
13 | |||
14 | return false | ||
15 | } | ||
16 | |||
17 | async function isMobileDevice () { | ||
18 | const caps = await browser.getCapabilities() | ||
19 | return caps.get('realMobile') === 'true' || caps.get('realMobile') === true | ||
20 | } | ||
21 | |||
22 | async function isSafari () { | ||
23 | const caps = await browser.getCapabilities() | ||
24 | return caps.get('browserName') && caps.get('browserName').toLowerCase() === 'safari' | ||
25 | } | ||
5 | 26 | ||
6 | describe('Videos workflow', () => { | 27 | describe('Videos workflow', () => { |
7 | let videoWatchPage: VideoWatchPage | 28 | let videoWatchPage: VideoWatchPage |
8 | let pageUploadPage: VideoUploadPage | 29 | let videoUploadPage: VideoUploadPage |
30 | let videoUpdatePage: VideoUpdatePage | ||
31 | let myAccountPage: MyAccountPage | ||
9 | let loginPage: LoginPage | 32 | let loginPage: LoginPage |
33 | |||
10 | const videoName = new Date().getTime() + ' video' | 34 | const videoName = new Date().getTime() + ' video' |
11 | let isMobileDevice = false | 35 | let videoWatchUrl: string |
12 | let isSafari = false | ||
13 | 36 | ||
14 | beforeEach(async () => { | 37 | beforeEach(async () => { |
15 | videoWatchPage = new VideoWatchPage() | 38 | videoWatchPage = new VideoWatchPage() |
16 | pageUploadPage = new VideoUploadPage() | 39 | videoUploadPage = new VideoUploadPage() |
40 | videoUpdatePage = new VideoUpdatePage() | ||
41 | myAccountPage = new MyAccountPage() | ||
17 | loginPage = new LoginPage() | 42 | loginPage = new LoginPage() |
18 | 43 | ||
19 | const caps = await browser.getCapabilities() | 44 | if (await isMobileDevice()) { |
20 | isMobileDevice = caps.get('realMobile') === 'true' || caps.get('realMobile') === true | ||
21 | isSafari = caps.get('browserName') && caps.get('browserName').toLowerCase() === 'safari' | ||
22 | |||
23 | if (isMobileDevice) { | ||
24 | console.log('Mobile device detected.') | 45 | console.log('Mobile device detected.') |
25 | } | 46 | } |
26 | 47 | ||
27 | if (isSafari) { | 48 | if (await isSafari()) { |
28 | console.log('Safari detected.') | 49 | console.log('Safari detected.') |
29 | } | 50 | } |
30 | }) | 51 | }) |
31 | 52 | ||
32 | it('Should log in', () => { | 53 | it('Should log in', async () => { |
33 | if (isMobileDevice || isSafari) { | 54 | if (await isMobileDevice() || await isSafari()) { |
34 | console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') | 55 | console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') |
35 | return | 56 | return |
36 | } | 57 | } |
@@ -39,24 +60,18 @@ describe('Videos workflow', () => { | |||
39 | }) | 60 | }) |
40 | 61 | ||
41 | it('Should upload a video', async () => { | 62 | it('Should upload a video', async () => { |
42 | if (isMobileDevice || isSafari) { | 63 | if (await skipIfUploadNotSupported()) return |
43 | console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') | ||
44 | return | ||
45 | } | ||
46 | 64 | ||
47 | await pageUploadPage.navigateTo() | 65 | await videoUploadPage.navigateTo() |
48 | 66 | ||
49 | await pageUploadPage.uploadVideo() | 67 | await videoUploadPage.uploadVideo() |
50 | return pageUploadPage.validSecondUploadStep(videoName) | 68 | return videoUploadPage.validSecondUploadStep(videoName) |
51 | }) | 69 | }) |
52 | 70 | ||
53 | it('Should list videos', async () => { | 71 | it('Should list videos', async () => { |
54 | await videoWatchPage.goOnVideosList(isMobileDevice, isSafari) | 72 | await videoWatchPage.goOnVideosList(await isMobileDevice(), await isSafari()) |
55 | 73 | ||
56 | if (isMobileDevice || isSafari) { | 74 | if (await skipIfUploadNotSupported()) return |
57 | console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') | ||
58 | return | ||
59 | } | ||
60 | 75 | ||
61 | const videoNames = videoWatchPage.getVideosListName() | 76 | const videoNames = videoWatchPage.getVideosListName() |
62 | expect(videoNames).toContain(videoName) | 77 | expect(videoNames).toContain(videoName) |
@@ -65,14 +80,16 @@ describe('Videos workflow', () => { | |||
65 | it('Should go on video watch page', async () => { | 80 | it('Should go on video watch page', async () => { |
66 | let videoNameToExcept = videoName | 81 | let videoNameToExcept = videoName |
67 | 82 | ||
68 | if (isMobileDevice || isSafari) videoNameToExcept = await videoWatchPage.clickOnFirstVideo() | 83 | if (await isMobileDevice() || await isSafari()) videoNameToExcept = await videoWatchPage.clickOnFirstVideo() |
69 | else await videoWatchPage.clickOnVideo(videoName) | 84 | else await videoWatchPage.clickOnVideo(videoName) |
70 | 85 | ||
71 | return videoWatchPage.waitWatchVideoName(videoNameToExcept, isMobileDevice, isSafari) | 86 | return videoWatchPage.waitWatchVideoName(videoNameToExcept, await isMobileDevice(), await isSafari()) |
72 | }) | 87 | }) |
73 | 88 | ||
74 | it('Should play the video', async () => { | 89 | it('Should play the video', async () => { |
75 | await videoWatchPage.playAndPauseVideo(true, isMobileDevice) | 90 | videoWatchUrl = await browser.getCurrentUrl() |
91 | |||
92 | await videoWatchPage.playAndPauseVideo(true, await isMobileDevice()) | ||
76 | expect(videoWatchPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) | 93 | expect(videoWatchPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) |
77 | }) | 94 | }) |
78 | 95 | ||
@@ -81,7 +98,7 @@ describe('Videos workflow', () => { | |||
81 | 98 | ||
82 | await videoWatchPage.goOnAssociatedEmbed() | 99 | await videoWatchPage.goOnAssociatedEmbed() |
83 | 100 | ||
84 | await videoWatchPage.playAndPauseVideo(false, isMobileDevice) | 101 | await videoWatchPage.playAndPauseVideo(false, await isMobileDevice()) |
85 | expect(videoWatchPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) | 102 | expect(videoWatchPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) |
86 | 103 | ||
87 | await browser.waitForAngularEnabled(true) | 104 | await browser.waitForAngularEnabled(true) |
@@ -92,9 +109,93 @@ describe('Videos workflow', () => { | |||
92 | 109 | ||
93 | await videoWatchPage.goOnP2PMediaLoaderEmbed() | 110 | await videoWatchPage.goOnP2PMediaLoaderEmbed() |
94 | 111 | ||
95 | await videoWatchPage.playAndPauseVideo(false, isMobileDevice) | 112 | await videoWatchPage.playAndPauseVideo(false, await isMobileDevice()) |
96 | expect(videoWatchPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) | 113 | expect(videoWatchPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) |
97 | 114 | ||
98 | await browser.waitForAngularEnabled(true) | 115 | await browser.waitForAngularEnabled(true) |
99 | }) | 116 | }) |
117 | |||
118 | it('Should update the video', async () => { | ||
119 | if (await skipIfUploadNotSupported()) return | ||
120 | |||
121 | await browser.get(videoWatchUrl) | ||
122 | |||
123 | await videoWatchPage.clickOnUpdate() | ||
124 | |||
125 | await videoUpdatePage.updateName('my new name') | ||
126 | |||
127 | await videoUpdatePage.validUpdate() | ||
128 | |||
129 | const name = await videoWatchPage.getVideoName() | ||
130 | expect(name).toEqual('my new name') | ||
131 | }) | ||
132 | |||
133 | it('Should add the video in my playlist', async () => { | ||
134 | if (await skipIfUploadNotSupported()) return | ||
135 | |||
136 | await videoWatchPage.clickOnSave() | ||
137 | await videoWatchPage.saveToWatchLater() | ||
138 | |||
139 | await videoUploadPage.navigateTo() | ||
140 | |||
141 | await videoUploadPage.uploadVideo() | ||
142 | await videoUploadPage.validSecondUploadStep('second video') | ||
143 | |||
144 | await videoWatchPage.clickOnSave() | ||
145 | await videoWatchPage.saveToWatchLater() | ||
146 | }) | ||
147 | |||
148 | it('Should have the watch later playlist in my account', async () => { | ||
149 | if (await skipIfUploadNotSupported()) return | ||
150 | |||
151 | await myAccountPage.navigateToMyPlaylists() | ||
152 | |||
153 | const name = await myAccountPage.getLastUpdatedPlaylistName() | ||
154 | expect(name).toEqual('Watch later') | ||
155 | |||
156 | const videosNumberText = await myAccountPage.getLastUpdatedPlaylistVideosText() | ||
157 | expect(videosNumberText).toEqual('2 videos') | ||
158 | |||
159 | await myAccountPage.clickOnLastUpdatedPlaylist() | ||
160 | |||
161 | const count = await myAccountPage.countTotalPlaylistElements() | ||
162 | expect(count).toEqual(2) | ||
163 | }) | ||
164 | |||
165 | it('Should watch the playlist', async () => { | ||
166 | if (await skipIfUploadNotSupported()) return | ||
167 | |||
168 | await myAccountPage.playPlaylist() | ||
169 | |||
170 | await videoWatchPage.waitUntilVideoName('second video', 20000 * 1000) | ||
171 | }) | ||
172 | |||
173 | it('Should have the video in my account', async () => { | ||
174 | if (await skipIfUploadNotSupported()) return | ||
175 | |||
176 | await myAccountPage.navigateToMyVideos() | ||
177 | |||
178 | const lastVideoName = await myAccountPage.getLastVideoName() | ||
179 | expect(lastVideoName).toEqual('second video') | ||
180 | }) | ||
181 | |||
182 | it('Should delete the last video', async () => { | ||
183 | if (await skipIfUploadNotSupported()) return | ||
184 | |||
185 | await myAccountPage.removeLastVideo() | ||
186 | await myAccountPage.validRemove() | ||
187 | |||
188 | const count = await myAccountPage.countVideos() | ||
189 | expect(count).toEqual(1) | ||
190 | }) | ||
191 | |||
192 | it('Should delete the first video', async () => { | ||
193 | if (await skipIfUploadNotSupported()) return | ||
194 | |||
195 | await myAccountPage.removeLastVideo() | ||
196 | await myAccountPage.validRemove() | ||
197 | |||
198 | const count = await myAccountPage.countVideos() | ||
199 | expect(count).toEqual(0) | ||
200 | }) | ||
100 | }) | 201 | }) |
diff --git a/client/proxy.config.json b/client/proxy.config.json index e5f0dfd61..4a72f1826 100644 --- a/client/proxy.config.json +++ b/client/proxy.config.json | |||
@@ -8,7 +8,8 @@ | |||
8 | "secure": false | 8 | "secure": false |
9 | }, | 9 | }, |
10 | "/socket.io": { | 10 | "/socket.io": { |
11 | "target": "http://localhost:9000", | 11 | "target": "ws://localhost:9000", |
12 | "secure": false | 12 | "secure": false, |
13 | "ws": true | ||
13 | } | 14 | } |
14 | } | 15 | } |
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html new file mode 100644 index 000000000..18689bbf7 --- /dev/null +++ b/client/src/app/+about/about-follows/about-follows.component.html | |||
@@ -0,0 +1,22 @@ | |||
1 | <div class="row" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> | ||
2 | <div class="col-xl-6 col-md-12"> | ||
3 | <div i18n class="subtitle">Followers</div> | ||
4 | |||
5 | <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have followers.</div> | ||
6 | |||
7 | <a *ngFor="let follower of followers" [href]="buildLink(follower)" target="_blank" rel="noopener noreferrer"> | ||
8 | {{ follower }} | ||
9 | </a> | ||
10 | </div> | ||
11 | |||
12 | <div class="col-xl-6 col-md-12"> | ||
13 | <div i18n class="subtitle">Followings</div> | ||
14 | |||
15 | <div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">This instance does not have followings.</div> | ||
16 | |||
17 | <a *ngFor="let following of followings" [href]="buildLink(following)" target="_blank" rel="noopener noreferrer"> | ||
18 | {{ following }} | ||
19 | </a> | ||
20 | </div> | ||
21 | |||
22 | </div> | ||
diff --git a/client/src/app/+about/about-follows/about-follows.component.scss b/client/src/app/+about/about-follows/about-follows.component.scss new file mode 100644 index 000000000..e0d597a96 --- /dev/null +++ b/client/src/app/+about/about-follows/about-follows.component.scss | |||
@@ -0,0 +1,14 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .subtitle { | ||
5 | font-size: 18px; | ||
6 | font-weight: $font-semibold; | ||
7 | margin-bottom: 20px; | ||
8 | } | ||
9 | |||
10 | a { | ||
11 | display: block; | ||
12 | width: fit-content; | ||
13 | margin-top: 3px; | ||
14 | } | ||
diff --git a/client/src/app/+about/about-follows/about-follows.component.ts b/client/src/app/+about/about-follows/about-follows.component.ts new file mode 100644 index 000000000..f0e1375d6 --- /dev/null +++ b/client/src/app/+about/about-follows/about-follows.component.ts | |||
@@ -0,0 +1,103 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { FollowService } from '@app/shared/instance/follow.service' | ||
3 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' | ||
4 | import { Notifier } from '@app/core' | ||
5 | import { RestService } from '@app/shared' | ||
6 | import { SortMeta } from 'primeng/api' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-about-follows', | ||
10 | templateUrl: './about-follows.component.html', | ||
11 | styleUrls: [ './about-follows.component.scss' ] | ||
12 | }) | ||
13 | |||
14 | export class AboutFollowsComponent implements OnInit { | ||
15 | followers: string[] = [] | ||
16 | followings: string[] = [] | ||
17 | |||
18 | followersPagination: ComponentPagination = { | ||
19 | currentPage: 1, | ||
20 | itemsPerPage: 40, | ||
21 | totalItems: null | ||
22 | } | ||
23 | |||
24 | followingsPagination: ComponentPagination = { | ||
25 | currentPage: 1, | ||
26 | itemsPerPage: 40, | ||
27 | totalItems: null | ||
28 | } | ||
29 | |||
30 | sort: SortMeta = { | ||
31 | field: 'createdAt', | ||
32 | order: -1 | ||
33 | } | ||
34 | |||
35 | constructor ( | ||
36 | private restService: RestService, | ||
37 | private notifier: Notifier, | ||
38 | private followService: FollowService | ||
39 | ) { } | ||
40 | |||
41 | ngOnInit () { | ||
42 | this.loadMoreFollowers() | ||
43 | |||
44 | this.loadMoreFollowings() | ||
45 | } | ||
46 | |||
47 | onNearOfBottom () { | ||
48 | this.onNearOfFollowersBottom() | ||
49 | |||
50 | this.onNearOfFollowingsBottom() | ||
51 | } | ||
52 | |||
53 | onNearOfFollowersBottom () { | ||
54 | if (!hasMoreItems(this.followersPagination)) return | ||
55 | |||
56 | this.followersPagination.currentPage += 1 | ||
57 | this.loadMoreFollowers() | ||
58 | } | ||
59 | |||
60 | onNearOfFollowingsBottom () { | ||
61 | if (!hasMoreItems(this.followingsPagination)) return | ||
62 | |||
63 | this.followingsPagination.currentPage += 1 | ||
64 | this.loadMoreFollowings() | ||
65 | } | ||
66 | |||
67 | buildLink (host: string) { | ||
68 | return window.location.protocol + '//' + host | ||
69 | } | ||
70 | |||
71 | private loadMoreFollowers () { | ||
72 | const pagination = this.restService.componentPaginationToRestPagination(this.followersPagination) | ||
73 | |||
74 | this.followService.getFollowers(pagination, this.sort) | ||
75 | .subscribe( | ||
76 | resultList => { | ||
77 | const newFollowers = resultList.data.map(r => r.follower.host) | ||
78 | this.followers = this.followers.concat(newFollowers) | ||
79 | |||
80 | this.followersPagination.totalItems = resultList.total | ||
81 | }, | ||
82 | |||
83 | err => this.notifier.error(err.message) | ||
84 | ) | ||
85 | } | ||
86 | |||
87 | private loadMoreFollowings () { | ||
88 | const pagination = this.restService.componentPaginationToRestPagination(this.followingsPagination) | ||
89 | |||
90 | this.followService.getFollowing(pagination, this.sort) | ||
91 | .subscribe( | ||
92 | resultList => { | ||
93 | const newFollowings = resultList.data.map(r => r.following.host) | ||
94 | this.followings = this.followings.concat(newFollowings) | ||
95 | |||
96 | this.followingsPagination.totalItems = resultList.total | ||
97 | }, | ||
98 | |||
99 | err => this.notifier.error(err.message) | ||
100 | ) | ||
101 | } | ||
102 | |||
103 | } | ||
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts index c83c62c7f..33e5070cb 100644 --- a/client/src/app/+about/about-routing.module.ts +++ b/client/src/app/+about/about-routing.module.ts | |||
@@ -4,6 +4,7 @@ import { MetaGuard } from '@ngx-meta/core' | |||
4 | import { AboutComponent } from './about.component' | 4 | import { AboutComponent } from './about.component' |
5 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | 5 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' |
6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | 6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' |
7 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' | ||
7 | 8 | ||
8 | const aboutRoutes: Routes = [ | 9 | const aboutRoutes: Routes = [ |
9 | { | 10 | { |
@@ -33,6 +34,15 @@ const aboutRoutes: Routes = [ | |||
33 | title: 'About PeerTube' | 34 | title: 'About PeerTube' |
34 | } | 35 | } |
35 | } | 36 | } |
37 | }, | ||
38 | { | ||
39 | path: 'follows', | ||
40 | component: AboutFollowsComponent, | ||
41 | data: { | ||
42 | meta: { | ||
43 | title: 'About follows' | ||
44 | } | ||
45 | } | ||
36 | } | 46 | } |
37 | ] | 47 | ] |
38 | } | 48 | } |
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html index 8c50835c1..0c4a5156d 100644 --- a/client/src/app/+about/about.component.html +++ b/client/src/app/+about/about.component.html | |||
@@ -5,10 +5,12 @@ | |||
5 | <a i18n routerLink="instance" routerLinkActive="active" class="title-page">Instance</a> | 5 | <a i18n routerLink="instance" routerLinkActive="active" class="title-page">Instance</a> |
6 | 6 | ||
7 | <a i18n routerLink="peertube" routerLinkActive="active" class="title-page">PeerTube</a> | 7 | <a i18n routerLink="peertube" routerLinkActive="active" class="title-page">PeerTube</a> |
8 | |||
9 | <a i18n routerLink="follows" routerLinkActive="active" class="title-page">Follows</a> | ||
8 | </div> | 10 | </div> |
9 | </div> | 11 | </div> |
10 | 12 | ||
11 | <div class="margin-content"> | 13 | <div class="margin-content"> |
12 | <router-outlet></router-outlet> | 14 | <router-outlet></router-outlet> |
13 | </div> | 15 | </div> |
14 | </div> \ No newline at end of file | 16 | </div> |
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts index 9c6b29740..49a7a52f8 100644 --- a/client/src/app/+about/about.module.ts +++ b/client/src/app/+about/about.module.ts | |||
@@ -6,6 +6,7 @@ import { SharedModule } from '../shared' | |||
6 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | 6 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' |
7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | 7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' |
8 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | 8 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' |
9 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' | ||
9 | 10 | ||
10 | @NgModule({ | 11 | @NgModule({ |
11 | imports: [ | 12 | imports: [ |
@@ -17,6 +18,7 @@ import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-a | |||
17 | AboutComponent, | 18 | AboutComponent, |
18 | AboutInstanceComponent, | 19 | AboutInstanceComponent, |
19 | AboutPeertubeComponent, | 20 | AboutPeertubeComponent, |
21 | AboutFollowsComponent, | ||
20 | ContactAdminModalComponent | 22 | ContactAdminModalComponent |
21 | ], | 23 | ], |
22 | 24 | ||
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index c3ef1d894..e9c8179b7 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -1,11 +1,25 @@ | |||
1 | <div *ngIf="account" class="row"> | 1 | <div class="margin-content"> |
2 | <a | 2 | |
3 | *ngFor="let videoChannel of videoChannels" [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" | 3 | <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> |
4 | class="video-channel" i18n-title title="See this video channel" | 4 | |
5 | > | 5 | <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"> |
6 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 6 | <div class="section channel" *ngFor="let videoChannel of videoChannels"> |
7 | 7 | <div class="section-title"> | |
8 | <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> | 8 | <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" i18n-title title="See this video channel"> |
9 | <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> | 9 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> |
10 | </a> | 10 | |
11 | </div> \ No newline at end of file | 11 | <div>{{ videoChannel.displayName }}</div> |
12 | <div i18n class="followers">{{ videoChannel.followersCount }} subscribers</div> | ||
13 | </a> | ||
14 | |||
15 | <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button> | ||
16 | </div> | ||
17 | |||
18 | <div *ngIf="getVideosOf(videoChannel)" class="videos"> | ||
19 | <div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div> | ||
20 | |||
21 | <my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> | ||
22 | </div> | ||
23 | </div> | ||
24 | </div> | ||
25 | </div> | ||
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index 0c6de2efa..98931f0c2 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss | |||
@@ -1,30 +1,28 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | ||
3 | 4 | ||
4 | .row { | 5 | .margin-content { |
5 | justify-content: center; | 6 | @include adapt-margin-content-width; |
6 | } | 7 | } |
7 | 8 | ||
8 | a.video-channel { | 9 | .section { |
9 | @include disable-default-a-behaviour; | 10 | @include miniature-rows; |
10 | 11 | ||
11 | display: inline-block; | 12 | overflow: visible; // For the subscribe dropdown |
12 | text-align: center; | 13 | padding-top: 0 !important; |
13 | color: var(--mainForegroundColor); | ||
14 | margin: 10px 30px; | ||
15 | 14 | ||
16 | img { | 15 | .section-title { |
17 | @include avatar(80px); | 16 | align-items: center; |
18 | |||
19 | margin-bottom: 10px; | ||
20 | } | 17 | } |
21 | 18 | ||
22 | .video-channel-display-name { | 19 | .videos { |
23 | font-size: 20px; | 20 | overflow: hidden; |
24 | font-weight: $font-bold; | ||
25 | } | ||
26 | 21 | ||
27 | .video-channel-followers { | 22 | .no-results { |
28 | font-size: 15px; | 23 | height: 50px; |
24 | } | ||
29 | } | 25 | } |
30 | } \ No newline at end of file | 26 | } |
27 | |||
28 | |||
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index 44f5626bb..a8d4237e8 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts | |||
@@ -3,9 +3,14 @@ import { ActivatedRoute } from '@angular/router' | |||
3 | import { Account } from '@app/shared/account/account.model' | 3 | import { Account } from '@app/shared/account/account.model' |
4 | import { AccountService } from '@app/shared/account/account.service' | 4 | import { AccountService } from '@app/shared/account/account.service' |
5 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 5 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
6 | import { flatMap, map, tap } from 'rxjs/operators' | 6 | import { concatMap, map, switchMap, tap } from 'rxjs/operators' |
7 | import { Subscription } from 'rxjs' | 7 | import { from, Subscription } from 'rxjs' |
8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
9 | import { Video } from '@app/shared/video/video.model' | ||
10 | import { AuthService } from '@app/core' | ||
11 | import { VideoService } from '@app/shared/video/video.service' | ||
12 | import { VideoSortField } from '@app/shared/video/sort-field.type' | ||
13 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' | ||
9 | 14 | ||
10 | @Component({ | 15 | @Component({ |
11 | selector: 'my-account-video-channels', | 16 | selector: 'my-account-video-channels', |
@@ -15,27 +20,73 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | |||
15 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | 20 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { |
16 | account: Account | 21 | account: Account |
17 | videoChannels: VideoChannel[] = [] | 22 | videoChannels: VideoChannel[] = [] |
23 | videos: { [id: number]: Video[] } = {} | ||
24 | |||
25 | channelPagination: ComponentPagination = { | ||
26 | currentPage: 1, | ||
27 | itemsPerPage: 2 | ||
28 | } | ||
29 | |||
30 | videosPagination: ComponentPagination = { | ||
31 | currentPage: 1, | ||
32 | itemsPerPage: 12 | ||
33 | } | ||
34 | videosSort: VideoSortField = '-publishedAt' | ||
18 | 35 | ||
19 | private accountSub: Subscription | 36 | private accountSub: Subscription |
20 | 37 | ||
21 | constructor ( | 38 | constructor ( |
22 | protected route: ActivatedRoute, | 39 | private route: ActivatedRoute, |
40 | private authService: AuthService, | ||
23 | private accountService: AccountService, | 41 | private accountService: AccountService, |
24 | private videoChannelService: VideoChannelService | 42 | private videoChannelService: VideoChannelService, |
43 | private videoService: VideoService | ||
25 | ) { } | 44 | ) { } |
26 | 45 | ||
46 | get user () { | ||
47 | return this.authService.getUser() | ||
48 | } | ||
49 | |||
27 | ngOnInit () { | 50 | ngOnInit () { |
28 | // Parent get the account for us | 51 | // Parent get the account for us |
29 | this.accountSub = this.accountService.accountLoaded | 52 | this.accountSub = this.accountService.accountLoaded |
30 | .pipe( | 53 | .subscribe(account => { |
31 | tap(account => this.account = account), | 54 | this.account = account |
32 | flatMap(account => this.videoChannelService.listAccountVideoChannels(account)), | 55 | |
33 | map(res => res.data) | 56 | this.loadMoreChannels() |
34 | ) | 57 | }) |
35 | .subscribe(videoChannels => this.videoChannels = videoChannels) | ||
36 | } | 58 | } |
37 | 59 | ||
38 | ngOnDestroy () { | 60 | ngOnDestroy () { |
39 | if (this.accountSub) this.accountSub.unsubscribe() | 61 | if (this.accountSub) this.accountSub.unsubscribe() |
40 | } | 62 | } |
63 | |||
64 | loadMoreChannels () { | ||
65 | this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination) | ||
66 | .pipe( | ||
67 | tap(res => this.channelPagination.totalItems = res.total), | ||
68 | switchMap(res => from(res.data)), | ||
69 | concatMap(videoChannel => { | ||
70 | return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort) | ||
71 | .pipe(map(data => ({ videoChannel, videos: data.videos }))) | ||
72 | }) | ||
73 | ) | ||
74 | .subscribe(({ videoChannel, videos }) => { | ||
75 | this.videoChannels.push(videoChannel) | ||
76 | |||
77 | this.videos[videoChannel.id] = videos | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | getVideosOf (videoChannel: VideoChannel) { | ||
82 | return this.videos[ videoChannel.id ] | ||
83 | } | ||
84 | |||
85 | onNearOfBottom () { | ||
86 | if (!hasMoreItems(this.channelPagination)) return | ||
87 | |||
88 | this.channelPagination.currentPage += 1 | ||
89 | |||
90 | this.loadMoreChannels() | ||
91 | } | ||
41 | } | 92 | } |
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 0d579fa0c..5a99aadce 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts | |||
@@ -29,6 +29,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
29 | private accountSub: Subscription | 29 | private accountSub: Subscription |
30 | 30 | ||
31 | constructor ( | 31 | constructor ( |
32 | protected i18n: I18n, | ||
32 | protected router: Router, | 33 | protected router: Router, |
33 | protected serverService: ServerService, | 34 | protected serverService: ServerService, |
34 | protected route: ActivatedRoute, | 35 | protected route: ActivatedRoute, |
@@ -36,13 +37,10 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
36 | protected notifier: Notifier, | 37 | protected notifier: Notifier, |
37 | protected confirmService: ConfirmService, | 38 | protected confirmService: ConfirmService, |
38 | protected screenService: ScreenService, | 39 | protected screenService: ScreenService, |
39 | private i18n: I18n, | ||
40 | private accountService: AccountService, | 40 | private accountService: AccountService, |
41 | private videoService: VideoService | 41 | private videoService: VideoService |
42 | ) { | 42 | ) { |
43 | super() | 43 | super() |
44 | |||
45 | this.titlePage = this.i18n('Published videos') | ||
46 | } | 44 | } |
47 | 45 | ||
48 | ngOnInit () { | 46 | ngOnInit () { |
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index 531d763c4..45b24eb55 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -8,13 +8,17 @@ import { AccountVideoChannelsComponent } from './account-video-channels/account- | |||
8 | 8 | ||
9 | const accountsRoutes: Routes = [ | 9 | const accountsRoutes: Routes = [ |
10 | { | 10 | { |
11 | path: 'peertube', | ||
12 | redirectTo: '/videos/local' | ||
13 | }, | ||
14 | { | ||
11 | path: ':accountId', | 15 | path: ':accountId', |
12 | component: AccountsComponent, | 16 | component: AccountsComponent, |
13 | canActivateChild: [ MetaGuard ], | 17 | canActivateChild: [ MetaGuard ], |
14 | children: [ | 18 | children: [ |
15 | { | 19 | { |
16 | path: '', | 20 | path: '', |
17 | redirectTo: 'videos', | 21 | redirectTo: 'video-channels', |
18 | pathMatch: 'full' | 22 | pathMatch: 'full' |
19 | }, | 23 | }, |
20 | { | 24 | { |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index c1377c1ea..038e18c4b 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -26,10 +26,10 @@ | |||
26 | </div> | 26 | </div> |
27 | 27 | ||
28 | <div class="links"> | 28 | <div class="links"> |
29 | <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> | ||
30 | |||
31 | <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a> | 29 | <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a> |
32 | 30 | ||
31 | <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> | ||
32 | |||
33 | <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> | 33 | <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> |
34 | </div> | 34 | </div> |
35 | </div> | 35 | </div> |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 71a4dfc4a..9ab883f60 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table' | |||
5 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
6 | import { AdminRoutingModule } from './admin-routing.module' | 6 | import { AdminRoutingModule } from './admin-routing.module' |
7 | import { AdminComponent } from './admin.component' | 7 | import { AdminComponent } from './admin.component' |
8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows' | 8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows' |
9 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 9 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' | 10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' |
11 | import { | 11 | import { |
@@ -66,7 +66,6 @@ import { DebugComponent, DebugService } from '@app/+admin/system/debug' | |||
66 | ], | 66 | ], |
67 | 67 | ||
68 | providers: [ | 68 | providers: [ |
69 | FollowService, | ||
70 | RedundancyService, | 69 | RedundancyService, |
71 | JobService, | 70 | JobService, |
72 | LogsService, | 71 | LogsService, |
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 637484622..d5b625d9c 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 | |||
@@ -287,6 +287,14 @@ | |||
287 | </div> | 287 | </div> |
288 | 288 | ||
289 | <div class="form-group"> | 289 | <div class="form-group"> |
290 | <my-peertube-checkbox | ||
291 | inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles" | ||
292 | i18n-labelText labelText="Allow audio files upload" | ||
293 | i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload" | ||
294 | ></my-peertube-checkbox> | ||
295 | </div> | ||
296 | |||
297 | <div class="form-group"> | ||
290 | <label i18n for="transcodingThreads">Transcoding threads</label> | 298 | <label i18n for="transcodingThreads">Transcoding threads</label> |
291 | <div class="peertube-select-container"> | 299 | <div class="peertube-select-container"> |
292 | <select id="transcodingThreads" formControlName="threads"> | 300 | <select id="transcodingThreads" formControlName="threads"> |
@@ -301,8 +309,8 @@ | |||
301 | <ng-container formGroupName="resolutions"> | 309 | <ng-container formGroupName="resolutions"> |
302 | <div class="form-group" *ngFor="let resolution of resolutions"> | 310 | <div class="form-group" *ngFor="let resolution of resolutions"> |
303 | <my-peertube-checkbox | 311 | <my-peertube-checkbox |
304 | [inputName]="getResolutionKey(resolution)" [formControlName]="resolution" | 312 | [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id" |
305 | i18n-labelText labelText="Resolution {{resolution}} enabled" | 313 | i18n-labelText labelText="Resolution {{resolution.label}} enabled" |
306 | ></my-peertube-checkbox> | 314 | ></my-peertube-checkbox> |
307 | </div> | 315 | </div> |
308 | </ng-container> | 316 | </ng-container> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index e64750713..055bae851 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -15,7 +15,7 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val | |||
15 | export class EditCustomConfigComponent extends FormReactive implements OnInit { | 15 | export class EditCustomConfigComponent extends FormReactive implements OnInit { |
16 | customConfig: CustomConfig | 16 | customConfig: CustomConfig |
17 | 17 | ||
18 | resolutions: string[] = [] | 18 | resolutions: { id: string, label: string }[] = [] |
19 | transcodingThreadOptions: { label: string, value: number }[] = [] | 19 | transcodingThreadOptions: { label: string, value: number }[] = [] |
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
@@ -30,11 +30,30 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
30 | super() | 30 | super() |
31 | 31 | ||
32 | this.resolutions = [ | 32 | this.resolutions = [ |
33 | this.i18n('240p'), | 33 | { |
34 | this.i18n('360p'), | 34 | id: '240p', |
35 | this.i18n('480p'), | 35 | label: this.i18n('240p') |
36 | this.i18n('720p'), | 36 | }, |
37 | this.i18n('1080p') | 37 | { |
38 | id: '360p', | ||
39 | label: this.i18n('360p') | ||
40 | }, | ||
41 | { | ||
42 | id: '480p', | ||
43 | label: this.i18n('480p') | ||
44 | }, | ||
45 | { | ||
46 | id: '720p', | ||
47 | label: this.i18n('720p') | ||
48 | }, | ||
49 | { | ||
50 | id: '1080p', | ||
51 | label: this.i18n('1080p') | ||
52 | }, | ||
53 | { | ||
54 | id: '2160p', | ||
55 | label: this.i18n('2160p') | ||
56 | } | ||
38 | ] | 57 | ] |
39 | 58 | ||
40 | this.transcodingThreadOptions = [ | 59 | this.transcodingThreadOptions = [ |
@@ -116,6 +135,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
116 | enabled: null, | 135 | enabled: null, |
117 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, | 136 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, |
118 | allowAdditionalExtensions: null, | 137 | allowAdditionalExtensions: null, |
138 | allowAudioFiles: null, | ||
119 | resolutions: {} | 139 | resolutions: {} |
120 | }, | 140 | }, |
121 | autoBlacklist: { | 141 | autoBlacklist: { |
@@ -139,8 +159,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
139 | } | 159 | } |
140 | } | 160 | } |
141 | for (const resolution of this.resolutions) { | 161 | for (const resolution of this.resolutions) { |
142 | defaultValues.transcoding.resolutions[resolution] = 'false' | 162 | defaultValues.transcoding.resolutions[resolution.id] = 'false' |
143 | formGroupData.transcoding.resolutions[resolution] = null | 163 | formGroupData.transcoding.resolutions[resolution.id] = null |
144 | } | 164 | } |
145 | 165 | ||
146 | this.buildForm(formGroupData) | 166 | this.buildForm(formGroupData) |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index b78cdf656..e25d9ab66 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts | |||
@@ -3,7 +3,7 @@ import { ConfirmService, Notifier } from '@app/core' | |||
3 | import { SortMeta } from 'primeng/primeng' | 3 | import { SortMeta } from 'primeng/primeng' |
4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' | 4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' |
5 | import { RestPagination, RestTable } from '../../../shared' | 5 | import { RestPagination, RestTable } from '../../../shared' |
6 | import { FollowService } from '../shared' | 6 | import { FollowService } from '@app/shared/instance/follow.service' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts index 2bb249746..308bbb0c5 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.ts +++ b/client/src/app/+admin/follows/following-add/following-add.component.ts | |||
@@ -3,7 +3,7 @@ import { Router } from '@angular/router' | |||
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService } from '../../../core' |
5 | import { validateHost } from '../../../shared' | 5 | import { validateHost } from '../../../shared' |
6 | import { FollowService } from '../shared' | 6 | import { FollowService } from '@app/shared/instance/follow.service' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index 4517a721e..ded616624 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts | |||
@@ -4,7 +4,7 @@ import { SortMeta } from 'primeng/primeng' | |||
4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' | 4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' |
5 | import { ConfirmService } from '../../../core/confirm/confirm.service' | 5 | import { ConfirmService } from '../../../core/confirm/confirm.service' |
6 | import { RestPagination, RestTable } from '../../../shared' | 6 | import { RestPagination, RestTable } from '../../../shared' |
7 | import { FollowService } from '../shared' | 7 | import { FollowService } from '@app/shared/instance/follow.service' |
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts index 7849a06e7..e94f33710 100644 --- a/client/src/app/+admin/follows/index.ts +++ b/client/src/app/+admin/follows/index.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | export * from './following-add' | 1 | export * from './following-add' |
2 | export * from './followers-list' | 2 | export * from './followers-list' |
3 | export * from './following-list' | 3 | export * from './following-list' |
4 | export * from './shared' | ||
5 | export * from './follows.component' | 4 | export * from './follows.component' |
6 | export * from './follows.routes' | 5 | export * from './follows.routes' |
diff --git a/client/src/app/+admin/follows/shared/index.ts b/client/src/app/+admin/follows/shared/index.ts deleted file mode 100644 index 78d456def..000000000 --- a/client/src/app/+admin/follows/shared/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './follow.service' | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index adce1b2d4..ee6d2c489 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -7,7 +7,8 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model' | |||
7 | export abstract class UserEdit extends FormReactive { | 7 | export abstract class UserEdit extends FormReactive { |
8 | videoQuotaOptions: { value: string, label: string }[] = [] | 8 | videoQuotaOptions: { value: string, label: string }[] = [] |
9 | videoQuotaDailyOptions: { value: string, label: string }[] = [] | 9 | videoQuotaDailyOptions: { value: string, label: string }[] = [] |
10 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) | 10 | roles = Object.keys(USER_ROLE_LABELS) |
11 | .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) | ||
11 | username: string | 12 | username: string |
12 | userId: number | 13 | userId: number |
13 | 14 | ||
@@ -27,7 +28,7 @@ export abstract class UserEdit extends FormReactive { | |||
27 | const transcodingConfig = this.serverService.getConfig().transcoding | 28 | const transcodingConfig = this.serverService.getConfig().transcoding |
28 | 29 | ||
29 | const resolutions = transcodingConfig.enabledResolutions | 30 | const resolutions = transcodingConfig.enabledResolutions |
30 | const higherResolution = VideoResolution.H_1080P | 31 | const higherResolution = VideoResolution.H_4K |
31 | let multiplier = 0 | 32 | let multiplier = 0 |
32 | 33 | ||
33 | for (const resolution of resolutions) { | 34 | for (const resolution of resolutions) { |
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts index 73340d21a..13607119e 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts | |||
@@ -27,6 +27,7 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn | |||
27 | videosHistoryEnabled: boolean | 27 | videosHistoryEnabled: boolean |
28 | 28 | ||
29 | constructor ( | 29 | constructor ( |
30 | protected i18n: I18n, | ||
30 | protected router: Router, | 31 | protected router: Router, |
31 | protected serverService: ServerService, | 32 | protected serverService: ServerService, |
32 | protected route: ActivatedRoute, | 33 | protected route: ActivatedRoute, |
@@ -34,7 +35,6 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn | |||
34 | protected userService: UserService, | 35 | protected userService: UserService, |
35 | protected notifier: Notifier, | 36 | protected notifier: Notifier, |
36 | protected screenService: ScreenService, | 37 | protected screenService: ScreenService, |
37 | protected i18n: I18n, | ||
38 | private confirmService: ConfirmService, | 38 | private confirmService: ConfirmService, |
39 | private videoService: VideoService, | 39 | private videoService: VideoService, |
40 | private userHistoryService: UserHistoryService | 40 | private userHistoryService: UserHistoryService |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/index.ts b/client/src/app/+my-account/my-account-settings/my-account-change-email/index.ts new file mode 100644 index 000000000..f42af361e --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './my-account-change-email.component' | |||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html new file mode 100644 index 000000000..5492cdf22 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html | |||
@@ -0,0 +1,36 @@ | |||
1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
2 | <div *ngIf="success" class="alert alert-success">{{ success }}</div> | ||
3 | |||
4 | <div i18n class="current-email"> | ||
5 | Your current email is <span class="email">{{ user.email }}</span> | ||
6 | </div> | ||
7 | |||
8 | <div i18n class="pending-email" *ngIf="user.pendingEmail"> | ||
9 | <span class="email">{{ user.pendingEmail }}</span> is awaiting email verification | ||
10 | </div> | ||
11 | |||
12 | <form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form"> | ||
13 | |||
14 | <div class="form-group"> | ||
15 | <label i18n for="new-email">New email</label> | ||
16 | <input | ||
17 | type="email" id="new-email" i18n-placeholder placeholder="Your new email" | ||
18 | formControlName="new-email" [ngClass]="{ 'input-error': formErrors['new-email'] }" | ||
19 | > | ||
20 | <div *ngIf="formErrors['new-email']" class="form-error"> | ||
21 | {{ formErrors['new-email'] }} | ||
22 | </div> | ||
23 | </div> | ||
24 | |||
25 | <div class="form-group"> | ||
26 | <input | ||
27 | type="password" id="password" i18n-placeholder placeholder="Your password" | ||
28 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
29 | > | ||
30 | <div *ngIf="formErrors['password']" class="form-error"> | ||
31 | {{ formErrors['password'] }} | ||
32 | </div> | ||
33 | </div> | ||
34 | |||
35 | <input type="submit" i18n-value value="Change email" [disabled]="!form.valid"> | ||
36 | </form> | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss new file mode 100644 index 000000000..81eba3ec9 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss | |||
@@ -0,0 +1,24 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input[type=password], | ||
5 | input[type=email] { | ||
6 | @include peertube-input-text(340px); | ||
7 | |||
8 | display: block; | ||
9 | } | ||
10 | |||
11 | input[type=submit] { | ||
12 | @include peertube-button; | ||
13 | @include orange-button; | ||
14 | } | ||
15 | |||
16 | .current-email, | ||
17 | .pending-email { | ||
18 | font-size: 16px; | ||
19 | margin: 15px 0; | ||
20 | |||
21 | .email { | ||
22 | font-weight: $font-semibold; | ||
23 | } | ||
24 | } | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts new file mode 100644 index 000000000..ec7cf935c --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts | |||
@@ -0,0 +1,73 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
3 | import { FormReactive, UserService } from '../../../shared' | ||
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
6 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
7 | import { User } from '../../../../../../shared' | ||
8 | import { tap } from 'rxjs/operators' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-account-change-email', | ||
12 | templateUrl: './my-account-change-email.component.html', | ||
13 | styleUrls: [ './my-account-change-email.component.scss' ] | ||
14 | }) | ||
15 | export class MyAccountChangeEmailComponent extends FormReactive implements OnInit { | ||
16 | error: string = null | ||
17 | success: string = null | ||
18 | user: User = null | ||
19 | |||
20 | constructor ( | ||
21 | protected formValidatorService: FormValidatorService, | ||
22 | private userValidatorsService: UserValidatorsService, | ||
23 | private notifier: Notifier, | ||
24 | private authService: AuthService, | ||
25 | private userService: UserService, | ||
26 | private serverService: ServerService, | ||
27 | private i18n: I18n | ||
28 | ) { | ||
29 | super() | ||
30 | } | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.buildForm({ | ||
34 | 'new-email': this.userValidatorsService.USER_EMAIL, | ||
35 | 'password': this.userValidatorsService.USER_PASSWORD | ||
36 | }) | ||
37 | |||
38 | this.user = this.authService.getUser() | ||
39 | } | ||
40 | |||
41 | changeEmail () { | ||
42 | this.error = null | ||
43 | this.success = null | ||
44 | |||
45 | const password = this.form.value[ 'password' ] | ||
46 | const email = this.form.value[ 'new-email' ] | ||
47 | |||
48 | this.userService.changeEmail(password, email) | ||
49 | .pipe( | ||
50 | tap(() => this.authService.refreshUserInformation()) | ||
51 | ) | ||
52 | .subscribe( | ||
53 | () => { | ||
54 | this.form.reset() | ||
55 | |||
56 | if (this.serverService.getConfig().signup.requiresEmailVerification) { | ||
57 | this.success = this.i18n('Please check your emails to verify your new email.') | ||
58 | } else { | ||
59 | this.success = this.i18n('Email updated.') | ||
60 | } | ||
61 | }, | ||
62 | |||
63 | err => { | ||
64 | if (err.status === 401) { | ||
65 | this.error = this.i18n('You current password is invalid.') | ||
66 | return | ||
67 | } | ||
68 | |||
69 | this.error = err.message | ||
70 | } | ||
71 | ) | ||
72 | } | ||
73 | } | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html index ae797d1bc..a39061ee3 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | <form role="form" (ngSubmit)="changePassword()" [formGroup]="form"> | 3 | <form role="form" (ngSubmit)="changePassword()" [formGroup]="form"> |
4 | 4 | ||
5 | <label i18n for="new-password">Change password</label> | 5 | <label i18n for="current-password">Change password</label> |
6 | <input | 6 | <input |
7 | type="password" id="current-password" i18n-placeholder placeholder="Current password" | 7 | type="password" id="current-password" i18n-placeholder placeholder="Current password" |
8 | formControlName="current-password" [ngClass]="{ 'input-error': formErrors['current-password'] }" | 8 | formControlName="current-password" [ngClass]="{ 'input-error': formErrors['current-password'] }" |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts index a9503ed1b..fcad5a6c2 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts | |||
@@ -30,7 +30,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit { | |||
30 | 30 | ||
31 | ngOnInit () { | 31 | ngOnInit () { |
32 | this.buildForm({ | 32 | this.buildForm({ |
33 | 'display-name': this.userValidatorsService.USER_DISPLAY_NAME, | 33 | 'display-name': this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED, |
34 | description: this.userValidatorsService.USER_DESCRIPTION | 34 | description: this.userValidatorsService.USER_DESCRIPTION |
35 | }) | 35 | }) |
36 | 36 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index ad64f28fe..f93d41110 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html | |||
@@ -13,6 +13,9 @@ | |||
13 | <div i18n class="account-title">Password</div> | 13 | <div i18n class="account-title">Password</div> |
14 | <my-account-change-password></my-account-change-password> | 14 | <my-account-change-password></my-account-change-password> |
15 | 15 | ||
16 | <div i18n class="account-title">Email</div> | ||
17 | <my-account-change-email></my-account-change-email> | ||
18 | |||
16 | <div i18n class="account-title">Video settings</div> | 19 | <div i18n class="account-title">Video settings</div> |
17 | <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> | 20 | <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> |
18 | 21 | ||
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html index 81fb11f45..f87df87df 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html | |||
@@ -61,5 +61,12 @@ When you will upload a video in this channel, the video support field will be au | |||
61 | </div> | 61 | </div> |
62 | </div> | 62 | </div> |
63 | 63 | ||
64 | <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()"> | ||
65 | <my-peertube-checkbox | ||
66 | inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate" | ||
67 | i18n-labelText labelText="Overwrite support field of all videos of this channel" | ||
68 | ></my-peertube-checkbox> | ||
69 | </div> | ||
70 | |||
64 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | 71 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> |
65 | </form> | 72 | </form> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts index 4dc65dd99..7479442d1 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts | |||
@@ -11,4 +11,9 @@ export abstract class MyAccountVideoChannelEdit extends FormReactive { | |||
11 | 11 | ||
12 | // FIXME: We need this method so angular does not complain in the child template | 12 | // FIXME: We need this method so angular does not complain in the child template |
13 | onAvatarChange (formData: FormData) { /* empty */ } | 13 | onAvatarChange (formData: FormData) { /* empty */ } |
14 | |||
15 | // Should be implemented by the child | ||
16 | isBulkUpdateVideosDisplayed () { | ||
17 | return false | ||
18 | } | ||
14 | } | 19 | } |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts index da4fb645a..081e956d2 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts | |||
@@ -20,6 +20,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
20 | videoChannelToUpdate: VideoChannel | 20 | videoChannelToUpdate: VideoChannel |
21 | 21 | ||
22 | private paramsSub: Subscription | 22 | private paramsSub: Subscription |
23 | private oldSupportField: string | ||
23 | 24 | ||
24 | constructor ( | 25 | constructor ( |
25 | protected formValidatorService: FormValidatorService, | 26 | protected formValidatorService: FormValidatorService, |
@@ -39,7 +40,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
39 | this.buildForm({ | 40 | this.buildForm({ |
40 | 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, | 41 | 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, |
41 | description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, | 42 | description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, |
42 | support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT | 43 | support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT, |
44 | bulkVideosSupportUpdate: null | ||
43 | }) | 45 | }) |
44 | 46 | ||
45 | this.paramsSub = this.route.params.subscribe(routeParams => { | 47 | this.paramsSub = this.route.params.subscribe(routeParams => { |
@@ -49,6 +51,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
49 | videoChannelToUpdate => { | 51 | videoChannelToUpdate => { |
50 | this.videoChannelToUpdate = videoChannelToUpdate | 52 | this.videoChannelToUpdate = videoChannelToUpdate |
51 | 53 | ||
54 | this.oldSupportField = videoChannelToUpdate.support | ||
55 | |||
52 | this.form.patchValue({ | 56 | this.form.patchValue({ |
53 | 'display-name': videoChannelToUpdate.displayName, | 57 | 'display-name': videoChannelToUpdate.displayName, |
54 | description: videoChannelToUpdate.description, | 58 | description: videoChannelToUpdate.description, |
@@ -72,7 +76,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
72 | const videoChannelUpdate: VideoChannelUpdate = { | 76 | const videoChannelUpdate: VideoChannelUpdate = { |
73 | displayName: body['display-name'], | 77 | displayName: body['display-name'], |
74 | description: body.description || null, | 78 | description: body.description || null, |
75 | support: body.support || null | 79 | support: body.support || null, |
80 | bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false | ||
76 | } | 81 | } |
77 | 82 | ||
78 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( | 83 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( |
@@ -118,4 +123,10 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
118 | getFormButtonTitle () { | 123 | getFormButtonTitle () { |
119 | return this.i18n('Update') | 124 | return this.i18n('Update') |
120 | } | 125 | } |
126 | |||
127 | isBulkUpdateVideosDisplayed () { | ||
128 | if (this.oldSupportField === undefined) return false | ||
129 | |||
130 | return this.oldSupportField !== this.form.value['support'] | ||
131 | } | ||
121 | } | 132 | } |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts index 87a10961f..8aed8b513 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts | |||
@@ -7,7 +7,6 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val | |||
7 | import { VideoPlaylistValidatorsService } from '@app/shared' | 7 | import { VideoPlaylistValidatorsService } from '@app/shared' |
8 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | 8 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' |
9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
10 | import { VideoConstant } from '@shared/models' | ||
11 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | 10 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' |
12 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | 11 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' |
13 | 12 | ||
@@ -18,7 +17,6 @@ import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | |||
18 | }) | 17 | }) |
19 | export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit { | 18 | export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit { |
20 | error: string | 19 | error: string |
21 | videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = [] | ||
22 | 20 | ||
23 | constructor ( | 21 | constructor ( |
24 | protected formValidatorService: FormValidatorService, | 22 | protected formValidatorService: FormValidatorService, |
@@ -47,6 +45,7 @@ export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylis | |||
47 | }) | 45 | }) |
48 | 46 | ||
49 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) | 47 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) |
48 | .catch(err => console.error('Cannot populate user video channels.', err)) | ||
50 | 49 | ||
51 | this.serverService.videoPlaylistPrivaciesLoaded.subscribe( | 50 | this.serverService.videoPlaylistPrivaciesLoaded.subscribe( |
52 | () => { | 51 | () => { |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html index 303fc46f7..82321459f 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html | |||
@@ -57,10 +57,12 @@ | |||
57 | </div> | 57 | </div> |
58 | 58 | ||
59 | <div class="form-group"> | 59 | <div class="form-group"> |
60 | <my-image-upload | 60 | <label i18n>Playlist thumbnail</label> |
61 | i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile" | 61 | |
62 | previewWidth="200px" previewHeight="110px" | 62 | <my-preview-upload |
63 | ></my-image-upload> | 63 | i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile" |
64 | previewWidth="223px" previewHeight="122px" | ||
65 | ></my-preview-upload> | ||
64 | </div> | 66 | </div> |
65 | </div> | 67 | </div> |
66 | </div> | 68 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts index fbfb4c8f7..e94188786 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { FormReactive } from '@app/shared' | 1 | import { FormReactive } from '@app/shared' |
2 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | ||
3 | import { ServerService } from '@app/core' | ||
4 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' | 2 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' |
3 | import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' | ||
5 | 4 | ||
6 | export abstract class MyAccountVideoPlaylistEdit extends FormReactive { | 5 | export abstract class MyAccountVideoPlaylistEdit extends FormReactive { |
7 | // Declare it here to avoid errors in create template | 6 | // Declare it here to avoid errors in create template |
8 | videoPlaylistToUpdate: VideoPlaylist | 7 | videoPlaylistToUpdate: VideoPlaylist |
9 | userVideoChannels: { id: number, label: string }[] = [] | 8 | userVideoChannels: { id: number, label: string }[] = [] |
9 | videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = [] | ||
10 | 10 | ||
11 | abstract isCreation (): boolean | 11 | abstract isCreation (): boolean |
12 | abstract getFormButtonTitle (): string | 12 | abstract getFormButtonTitle (): string |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts index 4887fdfb4..917ad7258 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts | |||
@@ -9,9 +9,8 @@ import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | |||
9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
10 | import { VideoPlaylistValidatorsService } from '@app/shared' | 10 | import { VideoPlaylistValidatorsService } from '@app/shared' |
11 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' | 11 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' |
12 | import { VideoConstant } from '@shared/models' | ||
13 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | ||
14 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 12 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
13 | import { delayWhen, map, switchMap } from 'rxjs/operators' | ||
15 | 14 | ||
16 | @Component({ | 15 | @Component({ |
17 | selector: 'my-account-video-playlist-update', | 16 | selector: 'my-account-video-playlist-update', |
@@ -21,7 +20,6 @@ import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | |||
21 | export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy { | 20 | export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy { |
22 | error: string | 21 | error: string |
23 | videoPlaylistToUpdate: VideoPlaylist | 22 | videoPlaylistToUpdate: VideoPlaylist |
24 | videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = [] | ||
25 | 23 | ||
26 | private paramsSub: Subscription | 24 | private paramsSub: Subscription |
27 | 25 | ||
@@ -53,31 +51,24 @@ export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylis | |||
53 | }) | 51 | }) |
54 | 52 | ||
55 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) | 53 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) |
56 | 54 | .catch(err => console.error('Cannot populate user video channels.', err)) | |
57 | this.paramsSub = this.route.params.subscribe(routeParams => { | 55 | |
58 | const videoPlaylistId = routeParams['videoPlaylistId'] | 56 | this.paramsSub = this.route.params |
59 | 57 | .pipe( | |
60 | this.videoPlaylistService.getVideoPlaylist(videoPlaylistId).subscribe( | 58 | map(routeParams => routeParams['videoPlaylistId']), |
61 | videoPlaylistToUpdate => { | 59 | switchMap(videoPlaylistId => this.videoPlaylistService.getVideoPlaylist(videoPlaylistId)), |
62 | this.videoPlaylistToUpdate = videoPlaylistToUpdate | 60 | delayWhen(() => this.serverService.videoPlaylistPrivaciesLoaded) |
63 | 61 | ) | |
64 | this.hydrateFormFromPlaylist() | 62 | .subscribe( |
65 | 63 | videoPlaylistToUpdate => { | |
66 | this.serverService.videoPlaylistPrivaciesLoaded.subscribe( | 64 | this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies() |
67 | () => { | 65 | this.videoPlaylistToUpdate = videoPlaylistToUpdate |
68 | this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies() | 66 | |
69 | .filter(p => { | 67 | this.hydrateFormFromPlaylist() |
70 | // If the playlist is not private, we cannot put it in private anymore | 68 | }, |
71 | return this.videoPlaylistToUpdate.privacy.id === VideoPlaylistPrivacy.PRIVATE || | 69 | |
72 | p.id !== VideoPlaylistPrivacy.PRIVATE | 70 | err => this.error = err.message |
73 | }) | 71 | ) |
74 | } | ||
75 | ) | ||
76 | }, | ||
77 | |||
78 | err => this.error = err.message | ||
79 | ) | ||
80 | }) | ||
81 | } | 72 | } |
82 | 73 | ||
83 | ngOnDestroy () { | 74 | ngOnDestroy () { |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index 84d464800..2854093c4 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -20,7 +20,7 @@ | |||
20 | <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> | 20 | <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> |
21 | 21 | ||
22 | <my-button i18n-label label="Change ownership" | 22 | <my-button i18n-label label="Change ownership" |
23 | className="action-button-change-ownership" | 23 | className="action-button-change-ownership grey-button" |
24 | icon="im-with-her" | 24 | icon="im-with-her" |
25 | (click)="changeOwnership($event, video)" | 25 | (click)="changeOwnership($event, video)" |
26 | ></my-button> | 26 | ></my-button> |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 4a18a9968..ca5b1f7cb 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -36,6 +36,7 @@ import { | |||
36 | MyAccountVideoPlaylistElementsComponent | 36 | MyAccountVideoPlaylistElementsComponent |
37 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' | 37 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' |
38 | import { DragDropModule } from '@angular/cdk/drag-drop' | 38 | import { DragDropModule } from '@angular/cdk/drag-drop' |
39 | import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' | ||
39 | 40 | ||
40 | @NgModule({ | 41 | @NgModule({ |
41 | imports: [ | 42 | imports: [ |
@@ -54,7 +55,10 @@ import { DragDropModule } from '@angular/cdk/drag-drop' | |||
54 | MyAccountChangePasswordComponent, | 55 | MyAccountChangePasswordComponent, |
55 | MyAccountVideoSettingsComponent, | 56 | MyAccountVideoSettingsComponent, |
56 | MyAccountProfileComponent, | 57 | MyAccountProfileComponent, |
58 | MyAccountChangeEmailComponent, | ||
59 | |||
57 | MyAccountVideosComponent, | 60 | MyAccountVideosComponent, |
61 | |||
58 | VideoChangeOwnershipComponent, | 62 | VideoChangeOwnershipComponent, |
59 | MyAccountOwnershipComponent, | 63 | MyAccountOwnershipComponent, |
60 | MyAccountAcceptOwnershipComponent, | 64 | MyAccountAcceptOwnershipComponent, |
diff --git a/client/src/app/+signup/+register/custom-stepper.component.html b/client/src/app/+signup/+register/custom-stepper.component.html new file mode 100644 index 000000000..bf507fc4f --- /dev/null +++ b/client/src/app/+signup/+register/custom-stepper.component.html | |||
@@ -0,0 +1,25 @@ | |||
1 | <section class="container"> | ||
2 | <header> | ||
3 | <ng-container *ngFor="let step of steps; let i = index; let isLast = last;"> | ||
4 | <div | ||
5 | class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step) }" | ||
6 | (click)="onClick(i)" | ||
7 | > | ||
8 | <div class="step-index"> | ||
9 | <ng-container *ngIf="!isCompleted(step)">{{ i + 1 }}</ng-container> | ||
10 | <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon> | ||
11 | </div> | ||
12 | |||
13 | <div class="step-label">{{ step.label }}</div> | ||
14 | </div> | ||
15 | |||
16 | <!-- Do no display if this is the last child --> | ||
17 | <div *ngIf="!isLast" class="connector"></div> | ||
18 | </ng-container> | ||
19 | </header> | ||
20 | |||
21 | <div [style.display]="selected ? 'block' : 'none'"> | ||
22 | <ng-container [ngTemplateOutlet]="selected.content"></ng-container> | ||
23 | </div> | ||
24 | |||
25 | </section> | ||
diff --git a/client/src/app/+signup/+register/custom-stepper.component.scss b/client/src/app/+signup/+register/custom-stepper.component.scss new file mode 100644 index 000000000..2371c8ae5 --- /dev/null +++ b/client/src/app/+signup/+register/custom-stepper.component.scss | |||
@@ -0,0 +1,66 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | $grey-color: #9CA3AB; | ||
5 | $index-block-height: 32px; | ||
6 | |||
7 | header { | ||
8 | display: flex; | ||
9 | justify-content: space-between; | ||
10 | font-size: 15px; | ||
11 | margin-bottom: 30px; | ||
12 | |||
13 | .step-info { | ||
14 | color: $grey-color; | ||
15 | display: flex; | ||
16 | flex-direction: column; | ||
17 | align-items: center; | ||
18 | width: $index-block-height; | ||
19 | |||
20 | .step-index { | ||
21 | display: flex; | ||
22 | justify-content: center; | ||
23 | align-items: center; | ||
24 | width: $index-block-height; | ||
25 | height: $index-block-height; | ||
26 | border-radius: 100px; | ||
27 | border: 2px solid $grey-color; | ||
28 | margin-bottom: 10px; | ||
29 | |||
30 | my-global-icon { | ||
31 | @include apply-svg-color(var(--mainBackgroundColor)); | ||
32 | |||
33 | width: 22px; | ||
34 | height: 22px; | ||
35 | } | ||
36 | } | ||
37 | |||
38 | .step-label { | ||
39 | width: max-content; | ||
40 | } | ||
41 | |||
42 | &.active, | ||
43 | &.completed { | ||
44 | .step-index { | ||
45 | border-color: var(--mainColor); | ||
46 | background-color: var(--mainColor); | ||
47 | color: var(--mainBackgroundColor); | ||
48 | } | ||
49 | |||
50 | .step-label { | ||
51 | color: var(--mainColor); | ||
52 | } | ||
53 | } | ||
54 | |||
55 | &.completed { | ||
56 | cursor: pointer; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | .connector { | ||
61 | flex: auto; | ||
62 | margin: $index-block-height/2 10px 0 10px; | ||
63 | height: 2px; | ||
64 | background-color: $grey-color; | ||
65 | } | ||
66 | } | ||
diff --git a/client/src/app/+signup/+register/custom-stepper.component.ts b/client/src/app/+signup/+register/custom-stepper.component.ts new file mode 100644 index 000000000..2ae40f3a9 --- /dev/null +++ b/client/src/app/+signup/+register/custom-stepper.component.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | import { CdkStep, CdkStepper } from '@angular/cdk/stepper' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-custom-stepper', | ||
6 | templateUrl: './custom-stepper.component.html', | ||
7 | styleUrls: [ './custom-stepper.component.scss' ], | ||
8 | providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ] | ||
9 | }) | ||
10 | export class CustomStepperComponent extends CdkStepper { | ||
11 | |||
12 | onClick (index: number): void { | ||
13 | this.selectedIndex = index | ||
14 | } | ||
15 | |||
16 | isCompleted (step: CdkStep) { | ||
17 | return step.stepControl && step.stepControl.dirty && step.stepControl.valid | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/signup/signup-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts index 820d16d4d..e3a5001dc 100644 --- a/client/src/app/signup/signup-routing.module.ts +++ b/client/src/app/+signup/+register/register-routing.module.ts | |||
@@ -1,17 +1,18 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { SignupComponent } from './signup.component' | 4 | import { RegisterComponent } from './register.component' |
5 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' | 5 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' |
6 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' | ||
6 | 7 | ||
7 | const signupRoutes: Routes = [ | 8 | const registerRoutes: Routes = [ |
8 | { | 9 | { |
9 | path: 'signup', | 10 | path: '', |
10 | component: SignupComponent, | 11 | component: RegisterComponent, |
11 | canActivate: [ MetaGuard ], | 12 | canActivate: [ MetaGuard, UnloggedGuard ], |
12 | data: { | 13 | data: { |
13 | meta: { | 14 | meta: { |
14 | title: 'Signup' | 15 | title: 'Register' |
15 | } | 16 | } |
16 | }, | 17 | }, |
17 | resolve: { | 18 | resolve: { |
@@ -21,7 +22,7 @@ const signupRoutes: Routes = [ | |||
21 | ] | 22 | ] |
22 | 23 | ||
23 | @NgModule({ | 24 | @NgModule({ |
24 | imports: [ RouterModule.forChild(signupRoutes) ], | 25 | imports: [ RouterModule.forChild(registerRoutes) ], |
25 | exports: [ RouterModule ] | 26 | exports: [ RouterModule ] |
26 | }) | 27 | }) |
27 | export class SignupRoutingModule {} | 28 | export class RegisterRoutingModule {} |
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html new file mode 100644 index 000000000..253374f87 --- /dev/null +++ b/client/src/app/+signup/+register/register-step-channel.component.html | |||
@@ -0,0 +1,54 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | |||
3 | <div class="channel-explanations"> | ||
4 | <p i18n> | ||
5 | A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.<br /> | ||
6 | For example, you could decide to have a channel to publish your piano concerts, and another channel in which you publish your videos talking about ecology. | ||
7 | </p> | ||
8 | |||
9 | <p> | ||
10 | Other users can decide to subscribe any channel they want, to be notified when you publish a new video. | ||
11 | </p> | ||
12 | </div> | ||
13 | |||
14 | <div class="form-group"> | ||
15 | <label for="displayName" i18n>Channel display name</label> | ||
16 | |||
17 | <div class="input-group"> | ||
18 | <input | ||
19 | type="text" id="displayName" | ||
20 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
21 | > | ||
22 | </div> | ||
23 | |||
24 | <div *ngIf="formErrors.displayName" class="form-error"> | ||
25 | {{ formErrors.displayName }} | ||
26 | </div> | ||
27 | </div> | ||
28 | |||
29 | <div class="form-group"> | ||
30 | <label for="name" i18n>Channel name</label> | ||
31 | |||
32 | <div class="input-group"> | ||
33 | <input | ||
34 | type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel" | ||
35 | formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" | ||
36 | > | ||
37 | <div class="input-group-append"> | ||
38 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
39 | </div> | ||
40 | </div> | ||
41 | |||
42 | <div class="name-information" i18n> | ||
43 | The channel name is a unique identifier of your channel on this instance. It's like an address mail, so other people can find your channel. | ||
44 | </div> | ||
45 | |||
46 | <div *ngIf="formErrors.name" class="form-error"> | ||
47 | {{ formErrors.name }} | ||
48 | </div> | ||
49 | |||
50 | <div *ngIf="isSameThanUsername()" class="form-error" i18n> | ||
51 | Channel name cannot be the same than your account name. You can click on the first step to update your account name. | ||
52 | </div> | ||
53 | </div> | ||
54 | </form> | ||
diff --git a/client/src/app/+signup/+register/register-step-channel.component.ts b/client/src/app/+signup/+register/register-step-channel.component.ts new file mode 100644 index 000000000..e434b91a7 --- /dev/null +++ b/client/src/app/+signup/+register/register-step-channel.component.ts | |||
@@ -0,0 +1,56 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { FormReactive, UserService, VideoChannelValidatorsService } from '@app/shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { FormGroup } from '@angular/forms' | ||
6 | import { pairwise } from 'rxjs/operators' | ||
7 | import { concat, of } from 'rxjs' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-register-step-channel', | ||
11 | templateUrl: './register-step-channel.component.html', | ||
12 | styleUrls: [ './register.component.scss' ] | ||
13 | }) | ||
14 | export class RegisterStepChannelComponent extends FormReactive implements OnInit { | ||
15 | @Input() username: string | ||
16 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
17 | |||
18 | constructor ( | ||
19 | protected formValidatorService: FormValidatorService, | ||
20 | private authService: AuthService, | ||
21 | private userService: UserService, | ||
22 | private videoChannelValidatorsService: VideoChannelValidatorsService | ||
23 | ) { | ||
24 | super() | ||
25 | } | ||
26 | |||
27 | get instanceHost () { | ||
28 | return window.location.host | ||
29 | } | ||
30 | |||
31 | ngOnInit () { | ||
32 | this.buildForm({ | ||
33 | displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, | ||
34 | name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME | ||
35 | }) | ||
36 | |||
37 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
38 | |||
39 | concat( | ||
40 | of(''), | ||
41 | this.form.get('displayName').valueChanges | ||
42 | ).pipe(pairwise()) | ||
43 | .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) | ||
44 | } | ||
45 | |||
46 | isSameThanUsername () { | ||
47 | return this.username && this.username === this.form.value['name'] | ||
48 | } | ||
49 | |||
50 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | ||
51 | const name = this.form.value['name'] || '' | ||
52 | |||
53 | const newName = this.userService.getNewUsername(oldDisplayName, newDisplayName, name) | ||
54 | this.form.patchValue({ name: newName }) | ||
55 | } | ||
56 | } | ||
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html new file mode 100644 index 000000000..47b3be8cc --- /dev/null +++ b/client/src/app/+signup/+register/register-step-user.component.html | |||
@@ -0,0 +1,73 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | |||
3 | <div class="form-group"> | ||
4 | <label for="displayName" i18n>Display name</label> | ||
5 | |||
6 | <div class="input-group"> | ||
7 | <input | ||
8 | type="text" id="displayName" placeholder="John Doe" | ||
9 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
10 | > | ||
11 | </div> | ||
12 | |||
13 | <div *ngIf="formErrors.displayName" class="form-error"> | ||
14 | {{ formErrors.displayName }} | ||
15 | </div> | ||
16 | </div> | ||
17 | |||
18 | <div class="form-group"> | ||
19 | <label for="username" i18n>Username</label> | ||
20 | |||
21 | <div class="input-group"> | ||
22 | <input | ||
23 | type="text" id="username" i18n-placeholder placeholder="Example: jane_doe" | ||
24 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
25 | > | ||
26 | <div class="input-group-append"> | ||
27 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
28 | </div> | ||
29 | </div> | ||
30 | |||
31 | <div class="name-information" i18n> | ||
32 | The username is a unique identifier of your account on this instance. It's like an address mail, so other people can find you. | ||
33 | </div> | ||
34 | |||
35 | <div *ngIf="formErrors.username" class="form-error"> | ||
36 | {{ formErrors.username }} | ||
37 | </div> | ||
38 | </div> | ||
39 | |||
40 | <div class="form-group"> | ||
41 | <label for="email" i18n>Email</label> | ||
42 | <input | ||
43 | type="text" id="email" i18n-placeholder placeholder="Email" | ||
44 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
45 | > | ||
46 | <div *ngIf="formErrors.email" class="form-error"> | ||
47 | {{ formErrors.email }} | ||
48 | </div> | ||
49 | </div> | ||
50 | |||
51 | <div class="form-group"> | ||
52 | <label for="password" i18n>Password</label> | ||
53 | <input | ||
54 | type="password" id="password" i18n-placeholder placeholder="Password" | ||
55 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
56 | > | ||
57 | <div *ngIf="formErrors.password" class="form-error"> | ||
58 | {{ formErrors.password }} | ||
59 | </div> | ||
60 | </div> | ||
61 | |||
62 | <div class="form-group form-group-terms"> | ||
63 | <my-peertube-checkbox | ||
64 | inputName="terms" formControlName="terms" | ||
65 | i18n-labelHtml | ||
66 | labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | ||
67 | ></my-peertube-checkbox> | ||
68 | |||
69 | <div *ngIf="formErrors.terms" class="form-error"> | ||
70 | {{ formErrors.terms }} | ||
71 | </div> | ||
72 | </div> | ||
73 | </form> | ||
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts new file mode 100644 index 000000000..3b71fd3c4 --- /dev/null +++ b/client/src/app/+signup/+register/register-step-user.component.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { FormReactive, UserService, UserValidatorsService } from '@app/shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { FormGroup } from '@angular/forms' | ||
6 | import { pairwise } from 'rxjs/operators' | ||
7 | import { concat, of } from 'rxjs' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-register-step-user', | ||
11 | templateUrl: './register-step-user.component.html', | ||
12 | styleUrls: [ './register.component.scss' ] | ||
13 | }) | ||
14 | export class RegisterStepUserComponent extends FormReactive implements OnInit { | ||
15 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
16 | |||
17 | constructor ( | ||
18 | protected formValidatorService: FormValidatorService, | ||
19 | private authService: AuthService, | ||
20 | private userService: UserService, | ||
21 | private userValidatorsService: UserValidatorsService | ||
22 | ) { | ||
23 | super() | ||
24 | } | ||
25 | |||
26 | get instanceHost () { | ||
27 | return window.location.host | ||
28 | } | ||
29 | |||
30 | ngOnInit () { | ||
31 | this.buildForm({ | ||
32 | displayName: this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED, | ||
33 | username: this.userValidatorsService.USER_USERNAME, | ||
34 | password: this.userValidatorsService.USER_PASSWORD, | ||
35 | email: this.userValidatorsService.USER_EMAIL, | ||
36 | terms: this.userValidatorsService.USER_TERMS | ||
37 | }) | ||
38 | |||
39 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
40 | |||
41 | concat( | ||
42 | of(''), | ||
43 | this.form.get('displayName').valueChanges | ||
44 | ).pipe(pairwise()) | ||
45 | .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) | ||
46 | } | ||
47 | |||
48 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | ||
49 | const username = this.form.value['username'] || '' | ||
50 | |||
51 | const newUsername = this.userService.getNewUsername(oldDisplayName, newDisplayName, username) | ||
52 | this.form.patchValue({ username: newUsername }) | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html new file mode 100644 index 000000000..d7e47c1a8 --- /dev/null +++ b/client/src/app/+signup/+register/register.component.html | |||
@@ -0,0 +1,47 @@ | |||
1 | <div class="margin-content"> | ||
2 | |||
3 | <div i18n class="title-page title-page-single"> | ||
4 | Create an account | ||
5 | </div> | ||
6 | |||
7 | <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success> | ||
8 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> | ||
9 | |||
10 | <div class="wrapper" *ngIf="!signupDone"> | ||
11 | <div> | ||
12 | <my-custom-stepper linear *ngIf="!signupDone"> | ||
13 | <cdk-step [stepControl]="formStepUser" i18n-label label="User information"> | ||
14 | <my-register-step-user (formBuilt)="onUserFormBuilt($event)"></my-register-step-user> | ||
15 | |||
16 | <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button> | ||
17 | </cdk-step> | ||
18 | |||
19 | <cdk-step [stepControl]="formStepChannel" i18n-label label="Channel information"> | ||
20 | <my-register-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-register-step-channel> | ||
21 | |||
22 | <button i18n cdkStepperNext (click)="signup()" | ||
23 | [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" | ||
24 | > | ||
25 | Create my account | ||
26 | </button> | ||
27 | </cdk-step> | ||
28 | |||
29 | <cdk-step i18n-label label="Done" editable="false"> | ||
30 | <div *ngIf="!signupDone && !error" class="done-loader"> | ||
31 | <my-loader [loading]="true"></my-loader> | ||
32 | |||
33 | <div i18n>PeerTube is creating your account...</div> | ||
34 | </div> | ||
35 | |||
36 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
37 | </cdk-step> | ||
38 | </my-custom-stepper> | ||
39 | </div> | ||
40 | |||
41 | <div> | ||
42 | <label i18n>Features found on this instance</label> | ||
43 | <my-instance-features-table></my-instance-features-table> | ||
44 | </div> | ||
45 | </div> | ||
46 | |||
47 | </div> | ||
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss new file mode 100644 index 000000000..8d14992e7 --- /dev/null +++ b/client/src/app/+signup/+register/register.component.scss | |||
@@ -0,0 +1,81 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .alert { | ||
5 | font-size: 15px; | ||
6 | text-align: center; | ||
7 | } | ||
8 | |||
9 | .wrapper { | ||
10 | display: flex; | ||
11 | justify-content: space-between; | ||
12 | flex-wrap: wrap; | ||
13 | |||
14 | & > div { | ||
15 | margin-bottom: 40px; | ||
16 | width: 450px; | ||
17 | |||
18 | @media screen and (max-width: 500px) { | ||
19 | width: auto; | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | |||
24 | my-instance-features-table { | ||
25 | display: block; | ||
26 | |||
27 | margin-bottom: 40px; | ||
28 | } | ||
29 | |||
30 | .form-group-terms { | ||
31 | margin: 30px 0; | ||
32 | } | ||
33 | |||
34 | .input-group { | ||
35 | @include peertube-input-group(400px); | ||
36 | } | ||
37 | |||
38 | .input-group-append { | ||
39 | height: 30px; | ||
40 | } | ||
41 | |||
42 | input:not([type=submit]) { | ||
43 | @include peertube-input-text(400px); | ||
44 | |||
45 | display: block; | ||
46 | |||
47 | &#username, | ||
48 | &#name { | ||
49 | width: auto !important; | ||
50 | flex-grow: 1; | ||
51 | } | ||
52 | } | ||
53 | |||
54 | input[type=submit], | ||
55 | button { | ||
56 | @include peertube-button; | ||
57 | @include orange-button; | ||
58 | } | ||
59 | |||
60 | .name-information { | ||
61 | margin-top: 10px; | ||
62 | } | ||
63 | |||
64 | .done-loader { | ||
65 | display: flex; | ||
66 | justify-content: center; | ||
67 | flex-direction: column; | ||
68 | align-items: center; | ||
69 | |||
70 | my-loader { | ||
71 | margin-bottom: 20px; | ||
72 | |||
73 | /deep/ .loader div { | ||
74 | border-color: var(--mainColor) transparent transparent transparent; | ||
75 | } | ||
76 | |||
77 | & + div { | ||
78 | font-size: 15px; | ||
79 | } | ||
80 | } | ||
81 | } | ||
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts new file mode 100644 index 000000000..cd6059728 --- /dev/null +++ b/client/src/app/+signup/+register/register.component.ts | |||
@@ -0,0 +1,89 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' | ||
3 | import { UserService, UserValidatorsService } from '@app/shared' | ||
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { UserRegister } from '@shared/models/users/user-register.model' | ||
6 | import { FormGroup } from '@angular/forms' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-register', | ||
10 | templateUrl: './register.component.html', | ||
11 | styleUrls: [ './register.component.scss' ] | ||
12 | }) | ||
13 | export class RegisterComponent { | ||
14 | info: string = null | ||
15 | error: string = null | ||
16 | success: string = null | ||
17 | signupDone = false | ||
18 | |||
19 | formStepUser: FormGroup | ||
20 | formStepChannel: FormGroup | ||
21 | |||
22 | constructor ( | ||
23 | private authService: AuthService, | ||
24 | private userValidatorsService: UserValidatorsService, | ||
25 | private notifier: Notifier, | ||
26 | private userService: UserService, | ||
27 | private serverService: ServerService, | ||
28 | private redirectService: RedirectService, | ||
29 | private i18n: I18n | ||
30 | ) { | ||
31 | } | ||
32 | |||
33 | get requiresEmailVerification () { | ||
34 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
35 | } | ||
36 | |||
37 | hasSameChannelAndAccountNames () { | ||
38 | return this.getUsername() === this.getChannelName() | ||
39 | } | ||
40 | |||
41 | getUsername () { | ||
42 | if (!this.formStepUser) return undefined | ||
43 | |||
44 | return this.formStepUser.value['username'] | ||
45 | } | ||
46 | |||
47 | getChannelName () { | ||
48 | if (!this.formStepChannel) return undefined | ||
49 | |||
50 | return this.formStepChannel.value['name'] | ||
51 | } | ||
52 | |||
53 | onUserFormBuilt (form: FormGroup) { | ||
54 | this.formStepUser = form | ||
55 | } | ||
56 | |||
57 | onChannelFormBuilt (form: FormGroup) { | ||
58 | this.formStepChannel = form | ||
59 | } | ||
60 | |||
61 | signup () { | ||
62 | this.error = null | ||
63 | |||
64 | const body: UserRegister = Object.assign(this.formStepUser.value, { channel: this.formStepChannel.value }) | ||
65 | |||
66 | this.userService.signup(body).subscribe( | ||
67 | () => { | ||
68 | this.signupDone = true | ||
69 | |||
70 | if (this.requiresEmailVerification) { | ||
71 | this.info = this.i18n('Now please check your emails to verify your account and complete signup.') | ||
72 | return | ||
73 | } | ||
74 | |||
75 | // Auto login | ||
76 | this.authService.login(body.username, body.password) | ||
77 | .subscribe( | ||
78 | () => { | ||
79 | this.success = this.i18n('You are now logged in as {{username}}!', { username: body.username }) | ||
80 | }, | ||
81 | |||
82 | err => this.error = err.message | ||
83 | ) | ||
84 | }, | ||
85 | |||
86 | err => this.error = err.message | ||
87 | ) | ||
88 | } | ||
89 | } | ||
diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts new file mode 100644 index 000000000..46336cbd0 --- /dev/null +++ b/client/src/app/+signup/+register/register.module.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RegisterRoutingModule } from './register-routing.module' | ||
3 | import { RegisterComponent } from './register.component' | ||
4 | import { SharedModule } from '@app/shared' | ||
5 | import { CdkStepperModule } from '@angular/cdk/stepper' | ||
6 | import { RegisterStepChannelComponent } from './register-step-channel.component' | ||
7 | import { RegisterStepUserComponent } from './register-step-user.component' | ||
8 | import { CustomStepperComponent } from './custom-stepper.component' | ||
9 | import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' | ||
10 | |||
11 | @NgModule({ | ||
12 | imports: [ | ||
13 | RegisterRoutingModule, | ||
14 | SharedModule, | ||
15 | CdkStepperModule, | ||
16 | SignupSharedModule | ||
17 | ], | ||
18 | |||
19 | declarations: [ | ||
20 | RegisterComponent, | ||
21 | CustomStepperComponent, | ||
22 | RegisterStepChannelComponent, | ||
23 | RegisterStepUserComponent | ||
24 | ], | ||
25 | |||
26 | exports: [ | ||
27 | RegisterComponent | ||
28 | ], | ||
29 | |||
30 | providers: [ | ||
31 | ] | ||
32 | }) | ||
33 | export class RegisterModule { } | ||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html index 2e4180632..2e4180632 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html | |||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss index efec6b706..efec6b706 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss | |||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index cfd471fa4..cfd471fa4 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html new file mode 100644 index 000000000..47519c943 --- /dev/null +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -0,0 +1,18 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div i18n class="title-page title-page-single"> | ||
3 | Verify account email confirmation | ||
4 | </div> | ||
5 | |||
6 | <my-signup-success i18n *ngIf="!isPendingEmail && success" message="Your email has been verified and you may now login."> | ||
7 | </my-signup-success> | ||
8 | |||
9 | <div i18n class="alert alert-success" *ngIf="isPendingEmail && success"> | ||
10 | Email updated. | ||
11 | </div> | ||
12 | |||
13 | <div *ngIf="failed"> | ||
14 | <span i18n>An error occurred.</span> | ||
15 | |||
16 | <a i18n routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email.</a> | ||
17 | </div> | ||
18 | </div> | ||
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index f9ecf664b..054f04310 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { Notifier } from '@app/core' | 4 | import { AuthService, Notifier } from '@app/core' |
5 | import { UserService } from '@app/shared' | 5 | import { UserService } from '@app/shared' |
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
@@ -11,12 +11,15 @@ import { UserService } from '@app/shared' | |||
11 | 11 | ||
12 | export class VerifyAccountEmailComponent implements OnInit { | 12 | export class VerifyAccountEmailComponent implements OnInit { |
13 | success = false | 13 | success = false |
14 | failed = false | ||
15 | isPendingEmail = false | ||
14 | 16 | ||
15 | private userId: number | 17 | private userId: number |
16 | private verificationString: string | 18 | private verificationString: string |
17 | 19 | ||
18 | constructor ( | 20 | constructor ( |
19 | private userService: UserService, | 21 | private userService: UserService, |
22 | private authService: AuthService, | ||
20 | private notifier: Notifier, | 23 | private notifier: Notifier, |
21 | private router: Router, | 24 | private router: Router, |
22 | private route: ActivatedRoute, | 25 | private route: ActivatedRoute, |
@@ -25,8 +28,12 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
25 | } | 28 | } |
26 | 29 | ||
27 | ngOnInit () { | 30 | ngOnInit () { |
28 | this.userId = this.route.snapshot.queryParams['userId'] | 31 | const queryParams = this.route.snapshot.queryParams |
29 | this.verificationString = this.route.snapshot.queryParams['verificationString'] | 32 | this.userId = queryParams['userId'] |
33 | this.verificationString = queryParams['verificationString'] | ||
34 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' | ||
35 | |||
36 | console.log(this.isPendingEmail) | ||
30 | 37 | ||
31 | if (!this.userId || !this.verificationString) { | 38 | if (!this.userId || !this.verificationString) { |
32 | this.notifier.error(this.i18n('Unable to find user id or verification string.')) | 39 | this.notifier.error(this.i18n('Unable to find user id or verification string.')) |
@@ -36,16 +43,17 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
36 | } | 43 | } |
37 | 44 | ||
38 | verifyEmail () { | 45 | verifyEmail () { |
39 | this.userService.verifyEmail(this.userId, this.verificationString) | 46 | this.userService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) |
40 | .subscribe( | 47 | .subscribe( |
41 | () => { | 48 | () => { |
49 | this.authService.refreshUserInformation() | ||
50 | |||
42 | this.success = true | 51 | this.success = true |
43 | setTimeout(() => { | ||
44 | this.router.navigate([ '/login' ]) | ||
45 | }, 2000) | ||
46 | }, | 52 | }, |
47 | 53 | ||
48 | err => { | 54 | err => { |
55 | this.failed = true | ||
56 | |||
49 | this.notifier.error(err.message) | 57 | this.notifier.error(err.message) |
50 | } | 58 | } |
51 | ) | 59 | ) |
diff --git a/client/src/app/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts index a038f0336..16d5fe0d0 100644 --- a/client/src/app/+verify-account/verify-account-routing.module.ts +++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts | |||
@@ -1,12 +1,8 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | |||
4 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
5 | 4 | import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' | |
6 | import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' | 5 | import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' |
7 | import { | ||
8 | VerifyAccountAskSendEmailComponent | ||
9 | } from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
10 | 6 | ||
11 | const verifyAccountRoutes: Routes = [ | 7 | const verifyAccountRoutes: Routes = [ |
12 | { | 8 | { |
diff --git a/client/src/app/+signup/+verify-account/verify-account.module.ts b/client/src/app/+signup/+verify-account/verify-account.module.ts new file mode 100644 index 000000000..9fe14e81e --- /dev/null +++ b/client/src/app/+signup/+verify-account/verify-account.module.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { VerifyAccountRoutingModule } from './verify-account-routing.module' | ||
3 | import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' | ||
4 | import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
5 | import { SharedModule } from '@app/shared' | ||
6 | import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' | ||
7 | |||
8 | @NgModule({ | ||
9 | imports: [ | ||
10 | VerifyAccountRoutingModule, | ||
11 | SharedModule, | ||
12 | SignupSharedModule | ||
13 | ], | ||
14 | |||
15 | declarations: [ | ||
16 | VerifyAccountEmailComponent, | ||
17 | VerifyAccountAskSendEmailComponent | ||
18 | ], | ||
19 | |||
20 | exports: [], | ||
21 | |||
22 | providers: [] | ||
23 | }) | ||
24 | export class VerifyAccountModule { | ||
25 | } | ||
diff --git a/client/src/app/+signup/shared/signup-shared.module.ts b/client/src/app/+signup/shared/signup-shared.module.ts new file mode 100644 index 000000000..cd21fdef3 --- /dev/null +++ b/client/src/app/+signup/shared/signup-shared.module.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SignupSuccessComponent } from '../shared/signup-success.component' | ||
3 | import { SharedModule } from '@app/shared' | ||
4 | |||
5 | @NgModule({ | ||
6 | imports: [ | ||
7 | SharedModule | ||
8 | ], | ||
9 | |||
10 | declarations: [ | ||
11 | SignupSuccessComponent | ||
12 | ], | ||
13 | |||
14 | exports: [ | ||
15 | SignupSuccessComponent | ||
16 | ], | ||
17 | |||
18 | providers: [ | ||
19 | ] | ||
20 | }) | ||
21 | export class SignupSharedModule { } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html new file mode 100644 index 000000000..e35f858c6 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success.component.html | |||
@@ -0,0 +1,16 @@ | |||
1 | <!-- Thanks: Amit Singh Sansoya from https://codepen.io/amit3200/pen/zWMJOO --> | ||
2 | |||
3 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2"> | ||
4 | <circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/> | ||
5 | <polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/> | ||
6 | </svg> | ||
7 | |||
8 | <p class="bottom-message">Welcome on PeerTube!</p> | ||
9 | |||
10 | <div *ngIf="message" class="alert alert-success"> | ||
11 | <p>{{ message }}</p> | ||
12 | |||
13 | <p i18n> | ||
14 | If you need help to use PeerTube, you can take a look to the <a href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
15 | </p> | ||
16 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.scss b/client/src/app/+signup/shared/signup-success.component.scss new file mode 100644 index 000000000..fbc27c8bc --- /dev/null +++ b/client/src/app/+signup/shared/signup-success.component.scss | |||
@@ -0,0 +1,76 @@ | |||
1 | svg { | ||
2 | width: 100px; | ||
3 | display: block; | ||
4 | margin: 40px auto 0; | ||
5 | } | ||
6 | |||
7 | .path { | ||
8 | stroke-dasharray: 1000; | ||
9 | stroke-dashoffset: 0; | ||
10 | |||
11 | &.circle { | ||
12 | -webkit-animation: dash .9s ease-in-out; | ||
13 | animation: dash .9s ease-in-out; | ||
14 | } | ||
15 | |||
16 | &.line { | ||
17 | stroke-dashoffset: 1000; | ||
18 | -webkit-animation: dash .9s .35s ease-in-out forwards; | ||
19 | animation: dash .9s .35s ease-in-out forwards; | ||
20 | } | ||
21 | |||
22 | &.check { | ||
23 | stroke-dashoffset: -100; | ||
24 | -webkit-animation: dash-check .9s .35s ease-in-out forwards; | ||
25 | animation: dash-check .9s .35s ease-in-out forwards; | ||
26 | } | ||
27 | } | ||
28 | |||
29 | .bottom-message { | ||
30 | text-align: center; | ||
31 | margin: 20px 0 60px; | ||
32 | font-size: 1.25em; | ||
33 | color: #73AF55; | ||
34 | } | ||
35 | |||
36 | .alert { | ||
37 | font-size: 15px; | ||
38 | text-align: center; | ||
39 | } | ||
40 | |||
41 | |||
42 | @-webkit-keyframes dash { | ||
43 | 0% { | ||
44 | stroke-dashoffset: 1000; | ||
45 | } | ||
46 | 100% { | ||
47 | stroke-dashoffset: 0; | ||
48 | } | ||
49 | } | ||
50 | |||
51 | @keyframes dash { | ||
52 | 0% { | ||
53 | stroke-dashoffset: 1000; | ||
54 | } | ||
55 | 100% { | ||
56 | stroke-dashoffset: 0; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | @-webkit-keyframes dash-check { | ||
61 | 0% { | ||
62 | stroke-dashoffset: -100; | ||
63 | } | ||
64 | 100% { | ||
65 | stroke-dashoffset: 900; | ||
66 | } | ||
67 | } | ||
68 | |||
69 | @keyframes dash-check { | ||
70 | 0% { | ||
71 | stroke-dashoffset: -100; | ||
72 | } | ||
73 | 100% { | ||
74 | stroke-dashoffset: 900; | ||
75 | } | ||
76 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts new file mode 100644 index 000000000..19fb5922a --- /dev/null +++ b/client/src/app/+signup/shared/signup-success.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success', | ||
5 | templateUrl: './signup-success.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessComponent { | ||
9 | @Input() message: string | ||
10 | } | ||
diff --git a/client/src/app/+verify-account/index.ts b/client/src/app/+verify-account/index.ts deleted file mode 100644 index 733f5ba77..000000000 --- a/client/src/app/+verify-account/index.ts +++ /dev/null | |||
@@ -1,2 +0,0 @@ | |||
1 | export * from '@app/+verify-account/verify-account-routing.module' | ||
2 | export * from '@app/+verify-account/verify-account.module' | ||
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html deleted file mode 100644 index a83d4a3c2..000000000 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div i18n class="title-page title-page-single"> | ||
3 | Verify account email confirmation | ||
4 | </div> | ||
5 | |||
6 | <div i18n *ngIf="success; else verificationError"> | ||
7 | Your email has been verified and you may now login. Redirecting... | ||
8 | </div> | ||
9 | <ng-template #verificationError> | ||
10 | <div> | ||
11 | <span i18n>An error occurred. </span> | ||
12 | <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a> | ||
13 | </div> | ||
14 | </ng-template> | ||
15 | </div> | ||
diff --git a/client/src/app/+verify-account/verify-account.module.ts b/client/src/app/+verify-account/verify-account.module.ts deleted file mode 100644 index 9092c6b4f..000000000 --- a/client/src/app/+verify-account/verify-account.module.ts +++ /dev/null | |||
@@ -1,27 +0,0 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | |||
3 | import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module' | ||
4 | import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' | ||
5 | import { | ||
6 | VerifyAccountAskSendEmailComponent | ||
7 | } from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
8 | import { SharedModule } from '@app/shared' | ||
9 | |||
10 | @NgModule({ | ||
11 | imports: [ | ||
12 | VerifyAccountRoutingModule, | ||
13 | SharedModule | ||
14 | ], | ||
15 | |||
16 | declarations: [ | ||
17 | VerifyAccountEmailComponent, | ||
18 | VerifyAccountAskSendEmailComponent | ||
19 | ], | ||
20 | |||
21 | exports: [ | ||
22 | ], | ||
23 | |||
24 | providers: [ | ||
25 | ] | ||
26 | }) | ||
27 | export class VerifyAccountModule { } | ||
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts index 907aefae1..7990044a2 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts | |||
@@ -5,7 +5,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | |||
5 | import { Subscription } from 'rxjs' | 5 | import { Subscription } from 'rxjs' |
6 | import { Notifier } from '@app/core' | 6 | import { Notifier } from '@app/core' |
7 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 7 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
8 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 8 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' |
9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
@@ -46,8 +46,7 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy { | |||
46 | } | 46 | } |
47 | 47 | ||
48 | onNearOfBottom () { | 48 | onNearOfBottom () { |
49 | // Last page | 49 | if (!hasMoreItems(this.pagination)) return |
50 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return | ||
51 | 50 | ||
52 | this.pagination.currentPage += 1 | 51 | this.pagination.currentPage += 1 |
53 | this.loadVideoPlaylists() | 52 | this.loadVideoPlaylists() |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 5e60b34b4..629fd4450 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -29,6 +29,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
29 | private videoChannelSub: Subscription | 29 | private videoChannelSub: Subscription |
30 | 30 | ||
31 | constructor ( | 31 | constructor ( |
32 | protected i18n: I18n, | ||
32 | protected router: Router, | 33 | protected router: Router, |
33 | protected serverService: ServerService, | 34 | protected serverService: ServerService, |
34 | protected route: ActivatedRoute, | 35 | protected route: ActivatedRoute, |
@@ -36,7 +37,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
36 | protected notifier: Notifier, | 37 | protected notifier: Notifier, |
37 | protected confirmService: ConfirmService, | 38 | protected confirmService: ConfirmService, |
38 | protected screenService: ScreenService, | 39 | protected screenService: ScreenService, |
39 | private i18n: I18n, | ||
40 | private videoChannelService: VideoChannelService, | 40 | private videoChannelService: VideoChannelService, |
41 | private videoService: VideoService | 41 | private videoService: VideoService |
42 | ) { | 42 | ) { |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index db8888dba..7ca51f226 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -16,7 +16,7 @@ const routes: Routes = [ | |||
16 | }, | 16 | }, |
17 | { | 17 | { |
18 | path: 'verify-account', | 18 | path: 'verify-account', |
19 | loadChildren: './+verify-account/verify-account.module#VerifyAccountModule' | 19 | loadChildren: './+signup/+verify-account/verify-account.module#VerifyAccountModule' |
20 | }, | 20 | }, |
21 | { | 21 | { |
22 | path: 'accounts', | 22 | path: 'accounts', |
@@ -31,6 +31,10 @@ const routes: Routes = [ | |||
31 | loadChildren: './+about/about.module#AboutModule' | 31 | loadChildren: './+about/about.module#AboutModule' |
32 | }, | 32 | }, |
33 | { | 33 | { |
34 | path: 'signup', | ||
35 | loadChildren: './+signup/+register/register.module#RegisterModule' | ||
36 | }, | ||
37 | { | ||
34 | path: '', | 38 | path: '', |
35 | component: AppComponent // Avoid 404, app component will redirect dynamically | 39 | component: AppComponent // Avoid 404, app component will redirect dynamically |
36 | }, | 40 | }, |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 0bbc2e08b..1e2936a37 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -14,7 +14,6 @@ import { HeaderComponent } from './header' | |||
14 | import { LoginModule } from './login' | 14 | import { LoginModule } from './login' |
15 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' | 15 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' |
16 | import { SharedModule } from './shared' | 16 | import { SharedModule } from './shared' |
17 | import { SignupModule } from './signup' | ||
18 | import { VideosModule } from './videos' | 17 | import { VideosModule } from './videos' |
19 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' | 18 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' |
20 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 19 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
@@ -53,7 +52,6 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
53 | CoreModule, | 52 | CoreModule, |
54 | LoginModule, | 53 | LoginModule, |
55 | ResetPasswordModule, | 54 | ResetPasswordModule, |
56 | SignupModule, | ||
57 | SearchModule, | 55 | SearchModule, |
58 | SharedModule, | 56 | SharedModule, |
59 | VideosModule, | 57 | VideosModule, |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index d3e72afb4..06fa8fcf1 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -20,6 +20,7 @@ import { Notifier } from './notification' | |||
20 | import { MessageService } from 'primeng/api' | 20 | import { MessageService } from 'primeng/api' |
21 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' | 21 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' |
22 | import { ServerConfigResolver } from './routing/server-config-resolver.service' | 22 | import { ServerConfigResolver } from './routing/server-config-resolver.service' |
23 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' | ||
23 | 24 | ||
24 | @NgModule({ | 25 | @NgModule({ |
25 | imports: [ | 26 | imports: [ |
@@ -58,6 +59,8 @@ import { ServerConfigResolver } from './routing/server-config-resolver.service' | |||
58 | ThemeService, | 59 | ThemeService, |
59 | LoginGuard, | 60 | LoginGuard, |
60 | UserRightGuard, | 61 | UserRightGuard, |
62 | UnloggedGuard, | ||
63 | |||
61 | RedirectService, | 64 | RedirectService, |
62 | Notifier, | 65 | Notifier, |
63 | MessageService, | 66 | MessageService, |
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts index e1db4097b..571822b76 100644 --- a/client/src/app/core/routing/redirect.service.ts +++ b/client/src/app/core/routing/redirect.service.ts | |||
@@ -42,7 +42,14 @@ export class RedirectService { | |||
42 | } | 42 | } |
43 | 43 | ||
44 | redirectToPreviousRoute () { | 44 | redirectToPreviousRoute () { |
45 | if (this.previousUrl) return this.router.navigateByUrl(this.previousUrl) | 45 | const exceptions = [ |
46 | '/verify-account' | ||
47 | ] | ||
48 | |||
49 | if (this.previousUrl) { | ||
50 | const isException = exceptions.find(e => this.previousUrl.startsWith(e)) | ||
51 | if (!isException) return this.router.navigateByUrl(this.previousUrl) | ||
52 | } | ||
46 | 53 | ||
47 | return this.redirectToHomepage() | 54 | return this.redirectToHomepage() |
48 | } | 55 | } |
diff --git a/client/src/app/core/routing/unlogged-guard.service.ts b/client/src/app/core/routing/unlogged-guard.service.ts new file mode 100644 index 000000000..3132a1a77 --- /dev/null +++ b/client/src/app/core/routing/unlogged-guard.service.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router' | ||
3 | import { AuthService } from '../auth/auth.service' | ||
4 | import { RedirectService } from './redirect.service' | ||
5 | |||
6 | @Injectable() | ||
7 | export class UnloggedGuard implements CanActivate, CanActivateChild { | ||
8 | |||
9 | constructor ( | ||
10 | private router: Router, | ||
11 | private auth: AuthService, | ||
12 | private redirectService: RedirectService | ||
13 | ) {} | ||
14 | |||
15 | canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | ||
16 | if (this.auth.isLoggedIn() === false) return true | ||
17 | |||
18 | this.redirectService.redirectToHomepage() | ||
19 | return false | ||
20 | } | ||
21 | |||
22 | canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | ||
23 | return this.canActivate(route, state) | ||
24 | } | ||
25 | } | ||
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 3a8a535fd..689f25a40 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -10,6 +10,7 @@ import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models | |||
10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
11 | import { sortBy } from '@app/shared/misc/utils' | 11 | import { sortBy } from '@app/shared/misc/utils' |
12 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | 12 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' |
13 | import { cloneDeep } from 'lodash-es' | ||
13 | 14 | ||
14 | @Injectable() | 15 | @Injectable() |
15 | export class ServerService { | 16 | export class ServerService { |
@@ -160,27 +161,27 @@ export class ServerService { | |||
160 | } | 161 | } |
161 | 162 | ||
162 | getConfig () { | 163 | getConfig () { |
163 | return this.config | 164 | return cloneDeep(this.config) |
164 | } | 165 | } |
165 | 166 | ||
166 | getVideoCategories () { | 167 | getVideoCategories () { |
167 | return this.videoCategories | 168 | return cloneDeep(this.videoCategories) |
168 | } | 169 | } |
169 | 170 | ||
170 | getVideoLicences () { | 171 | getVideoLicences () { |
171 | return this.videoLicences | 172 | return cloneDeep(this.videoLicences) |
172 | } | 173 | } |
173 | 174 | ||
174 | getVideoLanguages () { | 175 | getVideoLanguages () { |
175 | return this.videoLanguages | 176 | return cloneDeep(this.videoLanguages) |
176 | } | 177 | } |
177 | 178 | ||
178 | getVideoPrivacies () { | 179 | getVideoPrivacies () { |
179 | return this.videoPrivacies | 180 | return cloneDeep(this.videoPrivacies) |
180 | } | 181 | } |
181 | 182 | ||
182 | getVideoPlaylistPrivacies () { | 183 | getVideoPlaylistPrivacies () { |
183 | return this.videoPlaylistPrivacies | 184 | return cloneDeep(this.videoPlaylistPrivacies) |
184 | } | 185 | } |
185 | 186 | ||
186 | private loadAttributeEnum ( | 187 | private loadAttributeEnum ( |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index e80e6b803..588cb8548 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -63,7 +63,7 @@ | |||
63 | 63 | ||
64 | <a routerLink="/videos/overview" routerLinkActive="active"> | 64 | <a routerLink="/videos/overview" routerLinkActive="active"> |
65 | <my-global-icon iconName="globe"></my-global-icon> | 65 | <my-global-icon iconName="globe"></my-global-icon> |
66 | <ng-container i18n>Overview</ng-container> | 66 | <ng-container i18n>Discover</ng-container> |
67 | </a> | 67 | </a> |
68 | 68 | ||
69 | <a routerLink="/videos/trending" routerLinkActive="active"> | 69 | <a routerLink="/videos/trending" routerLinkActive="active"> |
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index 0a9f78cb2..055f64cc8 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html | |||
@@ -20,7 +20,7 @@ | |||
20 | </div> | 20 | </div> |
21 | </div> | 21 | </div> |
22 | 22 | ||
23 | <div class="results-filter" [ngbCollapse]="isSearchFilterCollapsed"> | 23 | <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed"> |
24 | <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters> | 24 | <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters> |
25 | </div> | 25 | </div> |
26 | </div> | 26 | </div> |
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss index 4e3ce1c96..3343a276d 100644 --- a/client/src/app/search/search.component.scss +++ b/client/src/app/search/search.component.scss | |||
@@ -35,18 +35,6 @@ | |||
35 | } | 35 | } |
36 | } | 36 | } |
37 | } | 37 | } |
38 | |||
39 | .results-filter { | ||
40 | // Animation when we show/hide the filters | ||
41 | transition: max-height 0.3s; | ||
42 | display: block !important; | ||
43 | overflow: hidden !important; | ||
44 | max-height: 0; | ||
45 | |||
46 | &.show { | ||
47 | max-height: 1500px; | ||
48 | } | ||
49 | } | ||
50 | } | 38 | } |
51 | 39 | ||
52 | .entry { | 40 | .entry { |
diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts index 0411fbe24..8b791621e 100644 --- a/client/src/app/search/search.module.ts +++ b/client/src/app/search/search.module.ts | |||
@@ -4,14 +4,11 @@ import { SearchComponent } from '@app/search/search.component' | |||
4 | import { SearchService } from '@app/search/search.service' | 4 | import { SearchService } from '@app/search/search.service' |
5 | import { SearchRoutingModule } from '@app/search/search-routing.module' | 5 | import { SearchRoutingModule } from '@app/search/search-routing.module' |
6 | import { SearchFiltersComponent } from '@app/search/search-filters.component' | 6 | import { SearchFiltersComponent } from '@app/search/search-filters.component' |
7 | import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap' | ||
8 | 7 | ||
9 | @NgModule({ | 8 | @NgModule({ |
10 | imports: [ | 9 | imports: [ |
11 | SearchRoutingModule, | 10 | SearchRoutingModule, |
12 | SharedModule, | 11 | SharedModule |
13 | |||
14 | NgbCollapseModule | ||
15 | ], | 12 | ], |
16 | 13 | ||
17 | declarations: [ | 14 | declarations: [ |
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index adecec1fc..5a517c975 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts | |||
@@ -4,7 +4,6 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | |||
4 | 4 | ||
5 | export abstract class Actor implements ActorServer { | 5 | export abstract class Actor implements ActorServer { |
6 | id: number | 6 | id: number |
7 | uuid: string | ||
8 | url: string | 7 | url: string |
9 | name: string | 8 | name: string |
10 | host: string | 9 | host: string |
@@ -35,7 +34,6 @@ export abstract class Actor implements ActorServer { | |||
35 | 34 | ||
36 | protected constructor (hash: ActorServer) { | 35 | protected constructor (hash: ActorServer) { |
37 | this.id = hash.id | 36 | this.id = hash.id |
38 | this.uuid = hash.uuid | ||
39 | this.url = hash.url | 37 | this.url = hash.url |
40 | this.name = hash.name | 38 | this.name = hash.name |
41 | this.host = hash.host | 39 | this.host = hash.host |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 04199a2a9..99d7f51c1 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -5,16 +5,9 @@ | |||
5 | @include peertube-button-link; | 5 | @include peertube-button-link; |
6 | @include button-with-icon(21px, 0, -2px); | 6 | @include button-with-icon(21px, 0, -2px); |
7 | 7 | ||
8 | font-weight: $font-semibold; | 8 | // FIXME: Firefox does not apply global .orange-button icon color |
9 | color: $grey-foreground-color; | 9 | &.orange-button { |
10 | background-color: $grey-background-color; | 10 | @include apply-svg-color(#fff) |
11 | |||
12 | &:hover { | ||
13 | background-color: $grey-background-hover-color; | ||
14 | } | ||
15 | |||
16 | my-global-icon { | ||
17 | @include apply-svg-color($grey-foreground-color); | ||
18 | } | 11 | } |
19 | } | 12 | } |
20 | 13 | ||
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index c2b69d31a..cf334e8d5 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -9,7 +9,7 @@ import { GlobalIconName } from '@app/shared/images/global-icon.component' | |||
9 | 9 | ||
10 | export class ButtonComponent { | 10 | export class ButtonComponent { |
11 | @Input() label = '' | 11 | @Input() label = '' |
12 | @Input() className: string = undefined | 12 | @Input() className = 'grey-button' |
13 | @Input() icon: GlobalIconName = undefined | 13 | @Input() icon: GlobalIconName = undefined |
14 | @Input() title: string = undefined | 14 | @Input() title: string = undefined |
15 | 15 | ||
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html index b4acb9d32..25196fbd5 100644 --- a/client/src/app/shared/buttons/delete-button.component.html +++ b/client/src/app/shared/buttons/delete-button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <span class="action-button action-button-delete" [title]="title" role="button"> | 1 | <span class="action-button action-button-delete grey-button" [title]="title" role="button"> |
2 | <my-global-icon iconName="delete"></my-global-icon> | 2 | <my-global-icon iconName="delete"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html index da3addbae..3d7cd4780 100644 --- a/client/src/app/shared/buttons/edit-button.component.html +++ b/client/src/app/shared/buttons/edit-button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> | 1 | <a class="action-button action-button-edit grey-button" [routerLink]="routerLink" i18n-title title="Edit"> |
2 | <my-global-icon iconName="edit"></my-global-icon> | 2 | <my-global-icon iconName="edit"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts index 6589b2580..2dafb1816 100644 --- a/client/src/app/shared/forms/form-validators/user-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts | |||
@@ -12,7 +12,7 @@ export class UserValidatorsService { | |||
12 | readonly USER_VIDEO_QUOTA: BuildFormValidator | 12 | readonly USER_VIDEO_QUOTA: BuildFormValidator |
13 | readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator | 13 | readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator |
14 | readonly USER_ROLE: BuildFormValidator | 14 | readonly USER_ROLE: BuildFormValidator |
15 | readonly USER_DISPLAY_NAME: BuildFormValidator | 15 | readonly USER_DISPLAY_NAME_REQUIRED: BuildFormValidator |
16 | readonly USER_DESCRIPTION: BuildFormValidator | 16 | readonly USER_DESCRIPTION: BuildFormValidator |
17 | readonly USER_TERMS: BuildFormValidator | 17 | readonly USER_TERMS: BuildFormValidator |
18 | 18 | ||
@@ -85,18 +85,7 @@ export class UserValidatorsService { | |||
85 | } | 85 | } |
86 | } | 86 | } |
87 | 87 | ||
88 | this.USER_DISPLAY_NAME = { | 88 | this.USER_DISPLAY_NAME_REQUIRED = this.getDisplayName(true) |
89 | VALIDATORS: [ | ||
90 | Validators.required, | ||
91 | Validators.minLength(1), | ||
92 | Validators.maxLength(50) | ||
93 | ], | ||
94 | MESSAGES: { | ||
95 | 'required': this.i18n('Display name is required.'), | ||
96 | 'minlength': this.i18n('Display name must be at least 1 character long.'), | ||
97 | 'maxlength': this.i18n('Display name cannot be more than 50 characters long.') | ||
98 | } | ||
99 | } | ||
100 | 89 | ||
101 | this.USER_DESCRIPTION = { | 90 | this.USER_DESCRIPTION = { |
102 | VALIDATORS: [ | 91 | VALIDATORS: [ |
@@ -129,4 +118,22 @@ export class UserValidatorsService { | |||
129 | } | 118 | } |
130 | } | 119 | } |
131 | } | 120 | } |
121 | |||
122 | private getDisplayName (required: boolean) { | ||
123 | const control = { | ||
124 | VALIDATORS: [ | ||
125 | Validators.minLength(1), | ||
126 | Validators.maxLength(120) | ||
127 | ], | ||
128 | MESSAGES: { | ||
129 | 'required': this.i18n('Display name is required.'), | ||
130 | 'minlength': this.i18n('Display name must be at least 1 character long.'), | ||
131 | 'maxlength': this.i18n('Display name cannot be more than 50 characters long.') | ||
132 | } | ||
133 | } | ||
134 | |||
135 | if (required) control.VALIDATORS.push(Validators.required) | ||
136 | |||
137 | return control | ||
138 | } | ||
132 | } | 139 | } |
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss index ea321ee65..84ea788af 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.scss +++ b/client/src/app/shared/forms/peertube-checkbox.component.scss | |||
@@ -14,9 +14,6 @@ | |||
14 | 14 | ||
15 | input { | 15 | input { |
16 | @include peertube-checkbox(1px); | 16 | @include peertube-checkbox(1px); |
17 | |||
18 | width: 10px; | ||
19 | margin-right: 10px; | ||
20 | } | 17 | } |
21 | } | 18 | } |
22 | 19 | ||
diff --git a/client/src/app/shared/forms/reactive-file.component.html b/client/src/app/shared/forms/reactive-file.component.html index 7d691059d..f6bf5f9ae 100644 --- a/client/src/app/shared/forms/reactive-file.component.html +++ b/client/src/app/shared/forms/reactive-file.component.html | |||
@@ -1,6 +1,9 @@ | |||
1 | <div class="root"> | 1 | <div class="root"> |
2 | <div class="button-file"> | 2 | <div class="button-file" [ngClass]="{ 'with-icon': !!icon }"> |
3 | <my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon> | ||
4 | |||
3 | <span>{{ inputLabel }}</span> | 5 | <span>{{ inputLabel }}</span> |
6 | |||
4 | <input | 7 | <input |
5 | type="file" | 8 | type="file" |
6 | [name]="inputName" [id]="inputName" [accept]="extensions" | 9 | [name]="inputName" [id]="inputName" [accept]="extensions" |
@@ -8,7 +11,5 @@ | |||
8 | /> | 11 | /> |
9 | </div> | 12 | </div> |
10 | 13 | ||
11 | <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div> | ||
12 | |||
13 | <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div> | 14 | <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div> |
14 | </div> | 15 | </div> |
diff --git a/client/src/app/shared/forms/reactive-file.component.scss b/client/src/app/shared/forms/reactive-file.component.scss index d89844264..84c23c1d6 100644 --- a/client/src/app/shared/forms/reactive-file.component.scss +++ b/client/src/app/shared/forms/reactive-file.component.scss | |||
@@ -8,13 +8,11 @@ | |||
8 | 8 | ||
9 | .button-file { | 9 | .button-file { |
10 | @include peertube-button-file(auto); | 10 | @include peertube-button-file(auto); |
11 | @include grey-button; | ||
11 | 12 | ||
12 | min-width: 190px; | 13 | &.with-icon { |
13 | } | 14 | @include button-with-icon; |
14 | 15 | } | |
15 | .file-constraints { | ||
16 | margin-left: 5px; | ||
17 | font-size: 13px; | ||
18 | } | 16 | } |
19 | 17 | ||
20 | .filename { | 18 | .filename { |
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts index f60c38e8d..b7a821d4f 100644 --- a/client/src/app/shared/forms/reactive-file.component.ts +++ b/client/src/app/shared/forms/reactive-file.component.ts | |||
@@ -2,6 +2,7 @@ import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@ang | |||
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { GlobalIconName } from '@app/shared/images/global-icon.component' | ||
5 | 6 | ||
6 | @Component({ | 7 | @Component({ |
7 | selector: 'my-reactive-file', | 8 | selector: 'my-reactive-file', |
@@ -21,6 +22,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor { | |||
21 | @Input() extensions: string[] = [] | 22 | @Input() extensions: string[] = [] |
22 | @Input() maxFileSize: number | 23 | @Input() maxFileSize: number |
23 | @Input() displayFilename = false | 24 | @Input() displayFilename = false |
25 | @Input() icon: GlobalIconName | ||
24 | 26 | ||
25 | @Output() fileChanged = new EventEmitter<Blob>() | 27 | @Output() fileChanged = new EventEmitter<Blob>() |
26 | 28 | ||
diff --git a/client/src/app/shared/images/image-upload.component.html b/client/src/app/shared/images/image-upload.component.html deleted file mode 100644 index c09c862c4..000000000 --- a/client/src/app/shared/images/image-upload.component.html +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | <div class="root"> | ||
2 | <my-reactive-file | ||
3 | [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize" | ||
4 | (fileChanged)="onFileChanged($event)" | ||
5 | ></my-reactive-file> | ||
6 | |||
7 | <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" /> | ||
8 | <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div> | ||
9 | </div> | ||
diff --git a/client/src/app/shared/images/image-upload.component.scss b/client/src/app/shared/images/image-upload.component.scss deleted file mode 100644 index b63963bca..000000000 --- a/client/src/app/shared/images/image-upload.component.scss +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .root { | ||
5 | height: auto; | ||
6 | display: flex; | ||
7 | align-items: center; | ||
8 | |||
9 | .preview { | ||
10 | border: 2px solid grey; | ||
11 | border-radius: 4px; | ||
12 | margin-left: 50px; | ||
13 | |||
14 | &.no-image { | ||
15 | background-color: #ececec; | ||
16 | } | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/shared/images/preview-upload.component.html b/client/src/app/shared/images/preview-upload.component.html new file mode 100644 index 000000000..5e1d5211b --- /dev/null +++ b/client/src/app/shared/images/preview-upload.component.html | |||
@@ -0,0 +1,13 @@ | |||
1 | <div class="root"> | ||
2 | <div class="preview-container"> | ||
3 | <my-reactive-file | ||
4 | [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize" | ||
5 | icon="edit" (fileChanged)="onFileChanged($event)" | ||
6 | ></my-reactive-file> | ||
7 | |||
8 | <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" /> | ||
9 | <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div> | ||
10 | </div> | ||
11 | |||
12 | <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxVideoImageSize | bytes }})</div> | ||
13 | </div> | ||
diff --git a/client/src/app/shared/images/preview-upload.component.scss b/client/src/app/shared/images/preview-upload.component.scss new file mode 100644 index 000000000..257060239 --- /dev/null +++ b/client/src/app/shared/images/preview-upload.component.scss | |||
@@ -0,0 +1,27 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .root { | ||
5 | height: auto; | ||
6 | display: flex; | ||
7 | flex-direction: column; | ||
8 | |||
9 | .preview-container { | ||
10 | position: relative; | ||
11 | |||
12 | my-reactive-file { | ||
13 | position: absolute; | ||
14 | bottom: 10px; | ||
15 | left: 10px; | ||
16 | } | ||
17 | |||
18 | .preview { | ||
19 | border: 2px solid grey; | ||
20 | border-radius: 4px; | ||
21 | |||
22 | &.no-image { | ||
23 | background-color: #ececec; | ||
24 | } | ||
25 | } | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/shared/images/image-upload.component.ts b/client/src/app/shared/images/preview-upload.component.ts index 2da1592ff..44b78866e 100644 --- a/client/src/app/shared/images/image-upload.component.ts +++ b/client/src/app/shared/images/preview-upload.component.ts | |||
@@ -1,27 +1,28 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, Input, OnInit } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | 3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' |
4 | import { ServerService } from '@app/core' | 4 | import { ServerService } from '@app/core' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-image-upload', | 7 | selector: 'my-preview-upload', |
8 | styleUrls: [ './image-upload.component.scss' ], | 8 | styleUrls: [ './preview-upload.component.scss' ], |
9 | templateUrl: './image-upload.component.html', | 9 | templateUrl: './preview-upload.component.html', |
10 | providers: [ | 10 | providers: [ |
11 | { | 11 | { |
12 | provide: NG_VALUE_ACCESSOR, | 12 | provide: NG_VALUE_ACCESSOR, |
13 | useExisting: forwardRef(() => ImageUploadComponent), | 13 | useExisting: forwardRef(() => PreviewUploadComponent), |
14 | multi: true | 14 | multi: true |
15 | } | 15 | } |
16 | ] | 16 | ] |
17 | }) | 17 | }) |
18 | export class ImageUploadComponent implements ControlValueAccessor { | 18 | export class PreviewUploadComponent implements OnInit, ControlValueAccessor { |
19 | @Input() inputLabel: string | 19 | @Input() inputLabel: string |
20 | @Input() inputName: string | 20 | @Input() inputName: string |
21 | @Input() previewWidth: string | 21 | @Input() previewWidth: string |
22 | @Input() previewHeight: string | 22 | @Input() previewHeight: string |
23 | 23 | ||
24 | imageSrc: SafeResourceUrl | 24 | imageSrc: SafeResourceUrl |
25 | allowedExtensionsMessage = '' | ||
25 | 26 | ||
26 | private file: File | 27 | private file: File |
27 | 28 | ||
@@ -38,6 +39,10 @@ export class ImageUploadComponent implements ControlValueAccessor { | |||
38 | return this.serverService.getConfig().video.image.size.max | 39 | return this.serverService.getConfig().video.image.size.max |
39 | } | 40 | } |
40 | 41 | ||
42 | ngOnInit () { | ||
43 | this.allowedExtensionsMessage = this.videoImageExtensions.join(', ') | ||
44 | } | ||
45 | |||
41 | onFileChanged (file: File) { | 46 | onFileChanged (file: File) { |
42 | this.file = file | 47 | this.file = file |
43 | 48 | ||
diff --git a/client/src/app/+admin/follows/shared/follow.service.ts b/client/src/app/shared/instance/follow.service.ts index c2b8ef006..5a44c64f1 100644 --- a/client/src/app/+admin/follows/shared/follow.service.ts +++ b/client/src/app/shared/instance/follow.service.ts | |||
@@ -3,13 +3,13 @@ import { HttpClient, HttpParams } from '@angular/common/http' | |||
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { SortMeta } from 'primeng/primeng' | 4 | import { SortMeta } from 'primeng/primeng' |
5 | import { Observable } from 'rxjs' | 5 | import { Observable } from 'rxjs' |
6 | import { ActorFollow, ResultList } from '../../../../../../shared' | 6 | import { ActorFollow, ResultList } from '@shared/index' |
7 | import { environment } from '../../../../environments/environment' | 7 | import { environment } from '../../../environments/environment' |
8 | import { RestExtractor, RestPagination, RestService } from '../../../shared' | 8 | import { RestExtractor, RestPagination, RestService } from '../rest' |
9 | 9 | ||
10 | @Injectable() | 10 | @Injectable() |
11 | export class FollowService { | 11 | export class FollowService { |
12 | private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/server' | 12 | private static BASE_APPLICATION_URL = 'https://peertube2.cpy.re' + '/api/v1/server' |
13 | 13 | ||
14 | constructor ( | 14 | constructor ( |
15 | private authHttp: HttpClient, | 15 | private authHttp: HttpClient, |
diff --git a/client/src/app/shared/misc/loader.component.html b/client/src/app/shared/misc/loader.component.html index b8b7ad343..ca8ed063e 100644 --- a/client/src/app/shared/misc/loader.component.html +++ b/client/src/app/shared/misc/loader.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <div *ngIf="loading"> | 1 | <div *ngIf="loading"> |
2 | <div class="lds-ring"> | 2 | <div class="loader"> |
3 | <div></div> | 3 | <div></div> |
4 | <div></div> | 4 | <div></div> |
5 | <div></div> | 5 | <div></div> |
diff --git a/client/src/app/shared/misc/loader.component.scss b/client/src/app/shared/misc/loader.component.scss index ddb64f07a..ffac9c707 100644 --- a/client/src/app/shared/misc/loader.component.scss +++ b/client/src/app/shared/misc/loader.component.scss | |||
@@ -3,14 +3,14 @@ | |||
3 | 3 | ||
4 | // Thanks to https://loading.io/css/ (CC0 License) | 4 | // Thanks to https://loading.io/css/ (CC0 License) |
5 | 5 | ||
6 | .lds-ring { | 6 | .loader { |
7 | display: inline-block; | 7 | display: inline-block; |
8 | position: relative; | 8 | position: relative; |
9 | width: 50px; | 9 | width: 50px; |
10 | height: 50px; | 10 | height: 50px; |
11 | } | 11 | } |
12 | 12 | ||
13 | .lds-ring div { | 13 | .loader div { |
14 | box-sizing: border-box; | 14 | box-sizing: border-box; |
15 | display: block; | 15 | display: block; |
16 | position: absolute; | 16 | position: absolute; |
@@ -19,23 +19,23 @@ | |||
19 | margin: 6px; | 19 | margin: 6px; |
20 | border: 4px solid; | 20 | border: 4px solid; |
21 | border-radius: 50%; | 21 | border-radius: 50%; |
22 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; | 22 | animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; |
23 | border-color: #999999 transparent transparent transparent; | 23 | border-color: #999999 transparent transparent transparent; |
24 | } | 24 | } |
25 | 25 | ||
26 | .lds-ring div:nth-child(1) { | 26 | .loader div:nth-child(1) { |
27 | animation-delay: -0.45s; | 27 | animation-delay: -0.45s; |
28 | } | 28 | } |
29 | 29 | ||
30 | .lds-ring div:nth-child(2) { | 30 | .loader div:nth-child(2) { |
31 | animation-delay: -0.3s; | 31 | animation-delay: -0.3s; |
32 | } | 32 | } |
33 | 33 | ||
34 | .lds-ring div:nth-child(3) { | 34 | .loader div:nth-child(3) { |
35 | animation-delay: -0.15s; | 35 | animation-delay: -0.15s; |
36 | } | 36 | } |
37 | 37 | ||
38 | @keyframes lds-ring { | 38 | @keyframes loader { |
39 | 0% { | 39 | 0% { |
40 | transform: rotate(0deg); | 40 | transform: rotate(0deg); |
41 | } | 41 | } |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index ded65653f..eb57a2fff 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -53,7 +53,14 @@ import { VideoCaptionService } from '@app/shared/video-caption' | |||
53 | import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component' | 53 | import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component' |
54 | import { VideoImportService } from '@app/shared/video-import/video-import.service' | 54 | import { VideoImportService } from '@app/shared/video-import/video-import.service' |
55 | import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' | 55 | import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' |
56 | import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' | 56 | import { |
57 | NgbCollapseModule, | ||
58 | NgbDropdownModule, | ||
59 | NgbModalModule, | ||
60 | NgbPopoverModule, | ||
61 | NgbTabsetModule, | ||
62 | NgbTooltipModule | ||
63 | } from '@ng-bootstrap/ng-bootstrap' | ||
57 | import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription' | 64 | import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription' |
58 | import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' | 65 | import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' |
59 | import { OverviewService } from '@app/shared/overview' | 66 | import { OverviewService } from '@app/shared/overview' |
@@ -69,7 +76,7 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha | |||
69 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' | 76 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' |
70 | import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' | 77 | import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' |
71 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 78 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
72 | import { ImageUploadComponent } from '@app/shared/images/image-upload.component' | 79 | import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component' |
73 | import { GlobalIconComponent } from '@app/shared/images/global-icon.component' | 80 | import { GlobalIconComponent } from '@app/shared/images/global-icon.component' |
74 | import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' | 81 | import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' |
75 | import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' | 82 | import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' |
@@ -85,6 +92,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis | |||
85 | import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' | 92 | import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' |
86 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' | 93 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' |
87 | import { ClipboardModule } from 'ngx-clipboard' | 94 | import { ClipboardModule } from 'ngx-clipboard' |
95 | import { FollowService } from '@app/shared/instance/follow.service' | ||
88 | 96 | ||
89 | @NgModule({ | 97 | @NgModule({ |
90 | imports: [ | 98 | imports: [ |
@@ -99,6 +107,7 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
99 | NgbPopoverModule, | 107 | NgbPopoverModule, |
100 | NgbTabsetModule, | 108 | NgbTabsetModule, |
101 | NgbTooltipModule, | 109 | NgbTooltipModule, |
110 | NgbCollapseModule, | ||
102 | 111 | ||
103 | ClipboardModule, | 112 | ClipboardModule, |
104 | 113 | ||
@@ -154,7 +163,7 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
154 | ConfirmComponent, | 163 | ConfirmComponent, |
155 | 164 | ||
156 | GlobalIconComponent, | 165 | GlobalIconComponent, |
157 | ImageUploadComponent | 166 | PreviewUploadComponent |
158 | ], | 167 | ], |
159 | 168 | ||
160 | exports: [ | 169 | exports: [ |
@@ -169,6 +178,7 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
169 | NgbPopoverModule, | 178 | NgbPopoverModule, |
170 | NgbTabsetModule, | 179 | NgbTabsetModule, |
171 | NgbTooltipModule, | 180 | NgbTooltipModule, |
181 | NgbCollapseModule, | ||
172 | 182 | ||
173 | ClipboardModule, | 183 | ClipboardModule, |
174 | 184 | ||
@@ -218,7 +228,7 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
218 | ConfirmComponent, | 228 | ConfirmComponent, |
219 | 229 | ||
220 | GlobalIconComponent, | 230 | GlobalIconComponent, |
221 | ImageUploadComponent, | 231 | PreviewUploadComponent, |
222 | 232 | ||
223 | NumberFormatterPipe, | 233 | NumberFormatterPipe, |
224 | ObjectLengthPipe, | 234 | ObjectLengthPipe, |
@@ -271,6 +281,8 @@ import { ClipboardModule } from 'ngx-clipboard' | |||
271 | 281 | ||
272 | UserNotificationService, | 282 | UserNotificationService, |
273 | 283 | ||
284 | FollowService, | ||
285 | |||
274 | I18n | 286 | I18n |
275 | ] | 287 | ] |
276 | }) | 288 | }) |
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss index 88f38d9cf..0c6c70d98 100644 --- a/client/src/app/shared/users/user-notifications.component.scss +++ b/client/src/app/shared/users/user-notifications.component.scss | |||
@@ -14,6 +14,7 @@ | |||
14 | font-size: inherit; | 14 | font-size: inherit; |
15 | padding: 15px 5px 15px 10px; | 15 | padding: 15px 5px 15px 10px; |
16 | border-bottom: 1px solid $separator-border-color; | 16 | border-bottom: 1px solid $separator-border-color; |
17 | word-break: break-word; | ||
17 | 18 | ||
18 | &.unread { | 19 | &.unread { |
19 | background-color: rgba(0, 0, 0, 0.05); | 20 | background-color: rgba(0, 0, 0, 0.05); |
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index e3ed2dfbd..14d13959a 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -8,6 +8,7 @@ export class User implements UserServerModel { | |||
8 | id: number | 8 | id: number |
9 | username: string | 9 | username: string |
10 | email: string | 10 | email: string |
11 | pendingEmail: string | null | ||
11 | emailVerified: boolean | 12 | emailVerified: boolean |
12 | nsfwPolicy: NSFWPolicyType | 13 | nsfwPolicy: NSFWPolicyType |
13 | 14 | ||
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index cc5c051f1..41ee87197 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -9,6 +9,7 @@ import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | |||
9 | import { SortMeta } from 'primeng/api' | 9 | import { SortMeta } from 'primeng/api' |
10 | import { BytesPipe } from 'ngx-pipes' | 10 | import { BytesPipe } from 'ngx-pipes' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { UserRegister } from '@shared/models/users/user-register.model' | ||
12 | 13 | ||
13 | @Injectable() | 14 | @Injectable() |
14 | export class UserService { | 15 | export class UserService { |
@@ -37,6 +38,20 @@ export class UserService { | |||
37 | ) | 38 | ) |
38 | } | 39 | } |
39 | 40 | ||
41 | changeEmail (password: string, newEmail: string) { | ||
42 | const url = UserService.BASE_USERS_URL + 'me' | ||
43 | const body: UserUpdateMe = { | ||
44 | currentPassword: password, | ||
45 | email: newEmail | ||
46 | } | ||
47 | |||
48 | return this.authHttp.put(url, body) | ||
49 | .pipe( | ||
50 | map(this.restExtractor.extractDataBool), | ||
51 | catchError(err => this.restExtractor.handleError(err)) | ||
52 | ) | ||
53 | } | ||
54 | |||
40 | updateMyProfile (profile: UserUpdateMe) { | 55 | updateMyProfile (profile: UserUpdateMe) { |
41 | const url = UserService.BASE_USERS_URL + 'me' | 56 | const url = UserService.BASE_USERS_URL + 'me' |
42 | 57 | ||
@@ -64,7 +79,7 @@ export class UserService { | |||
64 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 79 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
65 | } | 80 | } |
66 | 81 | ||
67 | signup (userCreate: UserCreate) { | 82 | signup (userCreate: UserRegister) { |
68 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 83 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
69 | .pipe( | 84 | .pipe( |
70 | map(this.restExtractor.extractDataBool), | 85 | map(this.restExtractor.extractDataBool), |
@@ -103,10 +118,11 @@ export class UserService { | |||
103 | ) | 118 | ) |
104 | } | 119 | } |
105 | 120 | ||
106 | verifyEmail (userId: number, verificationString: string) { | 121 | verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { |
107 | const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` | 122 | const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` |
108 | const body = { | 123 | const body = { |
109 | verificationString | 124 | verificationString, |
125 | isPendingEmail | ||
110 | } | 126 | } |
111 | 127 | ||
112 | return this.authHttp.post(url, body) | 128 | return this.authHttp.post(url, body) |
@@ -135,6 +151,22 @@ export class UserService { | |||
135 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 151 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
136 | } | 152 | } |
137 | 153 | ||
154 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { | ||
155 | // Don't update display name, the user seems to have changed it | ||
156 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername | ||
157 | |||
158 | return this.displayNameToUsername(newDisplayName) | ||
159 | } | ||
160 | |||
161 | displayNameToUsername (displayName: string) { | ||
162 | if (!displayName) return '' | ||
163 | |||
164 | return displayName | ||
165 | .toLowerCase() | ||
166 | .replace(/\s/g, '_') | ||
167 | .replace(/[^a-z0-9_.]/g, '') | ||
168 | } | ||
169 | |||
138 | /* ###### Admin methods ###### */ | 170 | /* ###### Admin methods ###### */ |
139 | 171 | ||
140 | addUser (userCreate: UserCreate) { | 172 | addUser (userCreate: UserCreate) { |
diff --git a/client/src/app/shared/video-channel/video-channel.service.ts b/client/src/app/shared/video-channel/video-channel.service.ts index d0bec649a..0168d37d9 100644 --- a/client/src/app/shared/video-channel/video-channel.service.ts +++ b/client/src/app/shared/video-channel/video-channel.service.ts | |||
@@ -2,7 +2,7 @@ import { catchError, map, tap } from 'rxjs/operators' | |||
2 | import { Injectable } from '@angular/core' | 2 | import { Injectable } from '@angular/core' |
3 | import { Observable, ReplaySubject } from 'rxjs' | 3 | import { Observable, ReplaySubject } from 'rxjs' |
4 | import { RestExtractor } from '../rest/rest-extractor.service' | 4 | import { RestExtractor } from '../rest/rest-extractor.service' |
5 | import { HttpClient } from '@angular/common/http' | 5 | import { HttpClient, HttpParams } from '@angular/common/http' |
6 | import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos' | 6 | import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos' |
7 | import { AccountService } from '../account/account.service' | 7 | import { AccountService } from '../account/account.service' |
8 | import { ResultList } from '../../../../../shared' | 8 | import { ResultList } from '../../../../../shared' |
@@ -10,6 +10,8 @@ import { VideoChannel } from './video-channel.model' | |||
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { Account } from '@app/shared/account/account.model' | 11 | import { Account } from '@app/shared/account/account.model' |
12 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 12 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
13 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
14 | import { RestService } from '@app/shared/rest' | ||
13 | 15 | ||
14 | @Injectable() | 16 | @Injectable() |
15 | export class VideoChannelService { | 17 | export class VideoChannelService { |
@@ -29,6 +31,7 @@ export class VideoChannelService { | |||
29 | 31 | ||
30 | constructor ( | 32 | constructor ( |
31 | private authHttp: HttpClient, | 33 | private authHttp: HttpClient, |
34 | private restService: RestService, | ||
32 | private restExtractor: RestExtractor | 35 | private restExtractor: RestExtractor |
33 | ) { } | 36 | ) { } |
34 | 37 | ||
@@ -41,8 +44,16 @@ export class VideoChannelService { | |||
41 | ) | 44 | ) |
42 | } | 45 | } |
43 | 46 | ||
44 | listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> { | 47 | listAccountVideoChannels (account: Account, componentPagination?: ComponentPagination): Observable<ResultList<VideoChannel>> { |
45 | return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels') | 48 | const pagination = componentPagination |
49 | ? this.restService.componentPaginationToRestPagination(componentPagination) | ||
50 | : { start: 0, count: 20 } | ||
51 | |||
52 | let params = new HttpParams() | ||
53 | params = this.restService.addRestGetParams(params, pagination) | ||
54 | |||
55 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' | ||
56 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | ||
46 | .pipe( | 57 | .pipe( |
47 | map(res => VideoChannelService.extractVideoChannels(res)), | 58 | map(res => VideoChannelService.extractVideoChannels(res)), |
48 | catchError(err => this.restExtractor.handleError(err)) | 59 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index 268677977..efd369bca 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -6,7 +6,7 @@ | |||
6 | </div> | 6 | </div> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | 9 | <my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed> |
10 | 10 | ||
11 | <div class="moderation-block" *ngIf="displayModerationBlock"> | 11 | <div class="moderation-block" *ngIf="displayModerationBlock"> |
12 | <my-peertube-checkbox | 12 | <my-peertube-checkbox |
@@ -22,11 +22,17 @@ | |||
22 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" | 22 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" |
23 | class="videos" | 23 | class="videos" |
24 | > | 24 | > |
25 | <my-video-miniature | 25 | <ng-container *ngFor="let video of videos; trackBy: videoById;"> |
26 | *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType" | 26 | <div class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> |
27 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" | 27 | {{ getCurrentGroupedDateLabel(video) }} |
28 | (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" | 28 | </div> |
29 | > | 29 | |
30 | </my-video-miniature> | 30 | <my-video-miniature |
31 | [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType" | ||
32 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" | ||
33 | (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" | ||
34 | > | ||
35 | </my-video-miniature> | ||
36 | </ng-container> | ||
31 | </div> | 37 | </div> |
32 | </div> | 38 | </div> |
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss index 9d481d6e4..98b80fdfd 100644 --- a/client/src/app/shared/video/abstract-video-list.scss +++ b/client/src/app/shared/video/abstract-video-list.scss | |||
@@ -24,33 +24,19 @@ | |||
24 | } | 24 | } |
25 | } | 25 | } |
26 | 26 | ||
27 | .margin-content { | 27 | .date-title { |
28 | width: $video-miniature-width * 6; | 28 | font-size: 16px; |
29 | margin: auto !important; | 29 | font-weight: $font-semibold; |
30 | 30 | margin-bottom: 20px; | |
31 | @media screen and (max-width: 1800px) { | 31 | margin-top: -10px; |
32 | width: $video-miniature-width * 5; | 32 | padding-top: 20px; |
33 | } | ||
34 | 33 | ||
35 | @media screen and (max-width: 1800px - $video-miniature-width) { | 34 | &:not(:first-child) { |
36 | width: $video-miniature-width * 4; | 35 | border-top: 1px solid $separator-border-color; |
37 | } | 36 | } |
37 | } | ||
38 | 38 | ||
39 | @media screen and (max-width: 1800px - (2* $video-miniature-width)) { | 39 | .margin-content { |
40 | width: $video-miniature-width * 3; | 40 | @include adapt-margin-content-width; |
41 | } | ||
42 | |||
43 | @media screen and (max-width: 1800px - (3* $video-miniature-width)) { | ||
44 | width: $video-miniature-width * 2; | ||
45 | } | ||
46 | |||
47 | @media screen and (max-width: 500px) { | ||
48 | width: auto; | ||
49 | margin: 0 !important; | ||
50 | |||
51 | .videos { | ||
52 | @include video-miniature-small-screen; | ||
53 | } | ||
54 | } | ||
55 | } | 41 | } |
56 | 42 | ||
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index fa9d38735..dc8f9cda9 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -11,6 +11,17 @@ import { MiniatureDisplayOptions, OwnerDisplayType } from '@app/shared/video/vid | |||
11 | import { Syndication } from '@app/shared/video/syndication.model' | 11 | import { Syndication } from '@app/shared/video/syndication.model' |
12 | import { Notifier, ServerService } from '@app/core' | 12 | import { Notifier, ServerService } from '@app/core' |
13 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 13 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
15 | import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' | ||
16 | |||
17 | enum GroupDate { | ||
18 | UNKNOWN = 0, | ||
19 | TODAY = 1, | ||
20 | YESTERDAY = 2, | ||
21 | LAST_WEEK = 3, | ||
22 | LAST_MONTH = 4, | ||
23 | OLDER = 5 | ||
24 | } | ||
14 | 25 | ||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { | 26 | export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { |
16 | pagination: ComponentPagination = { | 27 | pagination: ComponentPagination = { |
@@ -31,6 +42,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
31 | displayModerationBlock = false | 42 | displayModerationBlock = false |
32 | titleTooltip: string | 43 | titleTooltip: string |
33 | displayVideoActions = true | 44 | displayVideoActions = true |
45 | groupByDate = false | ||
34 | 46 | ||
35 | disabled = false | 47 | disabled = false |
36 | 48 | ||
@@ -50,11 +62,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
50 | protected abstract serverService: ServerService | 62 | protected abstract serverService: ServerService |
51 | protected abstract screenService: ScreenService | 63 | protected abstract screenService: ScreenService |
52 | protected abstract router: Router | 64 | protected abstract router: Router |
65 | protected abstract i18n: I18n | ||
53 | abstract titlePage: string | 66 | abstract titlePage: string |
54 | 67 | ||
55 | private resizeSubscription: Subscription | 68 | private resizeSubscription: Subscription |
56 | private angularState: number | 69 | private angularState: number |
57 | 70 | ||
71 | private groupedDateLabels: { [id in GroupDate]: string } | ||
72 | private groupedDates: { [id: number]: GroupDate } = {} | ||
73 | |||
58 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> | 74 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> |
59 | 75 | ||
60 | abstract generateSyndicationList (): void | 76 | abstract generateSyndicationList (): void |
@@ -64,6 +80,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
64 | } | 80 | } |
65 | 81 | ||
66 | ngOnInit () { | 82 | ngOnInit () { |
83 | this.groupedDateLabels = { | ||
84 | [GroupDate.UNKNOWN]: null, | ||
85 | [GroupDate.TODAY]: this.i18n('Today'), | ||
86 | [GroupDate.YESTERDAY]: this.i18n('Yesterday'), | ||
87 | [GroupDate.LAST_WEEK]: this.i18n('Last week'), | ||
88 | [GroupDate.LAST_MONTH]: this.i18n('Last month'), | ||
89 | [GroupDate.OLDER]: this.i18n('Older') | ||
90 | } | ||
91 | |||
67 | // Subscribe to route changes | 92 | // Subscribe to route changes |
68 | const routeParams = this.route.snapshot.queryParams | 93 | const routeParams = this.route.snapshot.queryParams |
69 | this.loadRouteParams(routeParams) | 94 | this.loadRouteParams(routeParams) |
@@ -113,6 +138,8 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
113 | this.pagination.totalItems = totalVideos | 138 | this.pagination.totalItems = totalVideos |
114 | this.videos = this.videos.concat(videos) | 139 | this.videos = this.videos.concat(videos) |
115 | 140 | ||
141 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
142 | |||
116 | this.onMoreVideos() | 143 | this.onMoreVideos() |
117 | }, | 144 | }, |
118 | 145 | ||
@@ -134,6 +161,59 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor | |||
134 | this.videos = this.videos.filter(v => v.id !== video.id) | 161 | this.videos = this.videos.filter(v => v.id !== video.id) |
135 | } | 162 | } |
136 | 163 | ||
164 | buildGroupedDateLabels () { | ||
165 | let currentGroupedDate: GroupDate = GroupDate.UNKNOWN | ||
166 | |||
167 | for (const video of this.videos) { | ||
168 | const publishedDate = video.publishedAt | ||
169 | |||
170 | if (currentGroupedDate <= GroupDate.TODAY && isToday(publishedDate)) { | ||
171 | if (currentGroupedDate === GroupDate.TODAY) continue | ||
172 | |||
173 | currentGroupedDate = GroupDate.TODAY | ||
174 | this.groupedDates[ video.id ] = currentGroupedDate | ||
175 | continue | ||
176 | } | ||
177 | |||
178 | if (currentGroupedDate <= GroupDate.YESTERDAY && isYesterday(publishedDate)) { | ||
179 | if (currentGroupedDate === GroupDate.YESTERDAY) continue | ||
180 | |||
181 | currentGroupedDate = GroupDate.YESTERDAY | ||
182 | this.groupedDates[ video.id ] = currentGroupedDate | ||
183 | continue | ||
184 | } | ||
185 | |||
186 | if (currentGroupedDate <= GroupDate.LAST_WEEK && isLastWeek(publishedDate)) { | ||
187 | if (currentGroupedDate === GroupDate.LAST_WEEK) continue | ||
188 | |||
189 | currentGroupedDate = GroupDate.LAST_WEEK | ||
190 | this.groupedDates[ video.id ] = currentGroupedDate | ||
191 | continue | ||
192 | } | ||
193 | |||
194 | if (currentGroupedDate <= GroupDate.LAST_MONTH && isLastMonth(publishedDate)) { | ||
195 | if (currentGroupedDate === GroupDate.LAST_MONTH) continue | ||
196 | |||
197 | currentGroupedDate = GroupDate.LAST_MONTH | ||
198 | this.groupedDates[ video.id ] = currentGroupedDate | ||
199 | continue | ||
200 | } | ||
201 | |||
202 | if (currentGroupedDate <= GroupDate.OLDER) { | ||
203 | if (currentGroupedDate === GroupDate.OLDER) continue | ||
204 | |||
205 | currentGroupedDate = GroupDate.OLDER | ||
206 | this.groupedDates[ video.id ] = currentGroupedDate | ||
207 | } | ||
208 | } | ||
209 | } | ||
210 | |||
211 | getCurrentGroupedDateLabel (video: Video) { | ||
212 | if (this.groupByDate === false) return undefined | ||
213 | |||
214 | return this.groupedDateLabels[this.groupedDates[video.id]] | ||
215 | } | ||
216 | |||
137 | // On videos hook for children that want to do something | 217 | // On videos hook for children that want to do something |
138 | protected onMoreVideos () { /* empty */ } | 218 | protected onMoreVideos () { /* empty */ } |
139 | 219 | ||
diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html index dd01c1388..935d01330 100644 --- a/client/src/app/shared/video/modals/video-download.component.html +++ b/client/src/app/shared/video/modals/video-download.component.html | |||
@@ -31,11 +31,6 @@ | |||
31 | <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> | 31 | <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> |
32 | <label i18n for="download-torrent">Torrent (.torrent file)</label> | 32 | <label i18n for="download-torrent">Torrent (.torrent file)</label> |
33 | </div> | 33 | </div> |
34 | |||
35 | <div class="peertube-radio-container"> | ||
36 | <input type="radio" name="download" id="download-magnet" [(ngModel)]="downloadType" value="magnet"> | ||
37 | <label i18n for="download-magnet">Torrent (magnet link)</label> | ||
38 | </div> | ||
39 | </div> | 34 | </div> |
40 | </div> | 35 | </div> |
41 | 36 | ||
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts index d6d10d29e..16f3621b4 100644 --- a/client/src/app/shared/video/modals/video-download.component.ts +++ b/client/src/app/shared/video/modals/video-download.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, ElementRef, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, ViewChild } from '@angular/core' |
2 | import { VideoDetails } from '../../../shared/video/video-details.model' | 2 | import { VideoDetails } from '../../../shared/video/video-details.model' |
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 3 | import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { Notifier } from '@app/core' | 5 | import { Notifier } from '@app/core' |
6 | 6 | ||
@@ -12,10 +12,11 @@ import { Notifier } from '@app/core' | |||
12 | export class VideoDownloadComponent { | 12 | export class VideoDownloadComponent { |
13 | @ViewChild('modal') modal: ElementRef | 13 | @ViewChild('modal') modal: ElementRef |
14 | 14 | ||
15 | downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent' | 15 | downloadType: 'direct' | 'torrent' = 'torrent' |
16 | resolutionId: number | string = -1 | 16 | resolutionId: number | string = -1 |
17 | 17 | ||
18 | video: VideoDetails | 18 | video: VideoDetails |
19 | activeModal: NgbActiveModal | ||
19 | 20 | ||
20 | constructor ( | 21 | constructor ( |
21 | private notifier: Notifier, | 22 | private notifier: Notifier, |
@@ -26,9 +27,7 @@ export class VideoDownloadComponent { | |||
26 | show (video: VideoDetails) { | 27 | show (video: VideoDetails) { |
27 | this.video = video | 28 | this.video = video |
28 | 29 | ||
29 | const m = this.modalService.open(this.modal) | 30 | this.activeModal = this.modalService.open(this.modal) |
30 | m.result.then(() => this.onClose()) | ||
31 | .catch(() => this.onClose()) | ||
32 | 31 | ||
33 | this.resolutionId = this.video.files[0].resolution.id | 32 | this.resolutionId = this.video.files[0].resolution.id |
34 | } | 33 | } |
@@ -39,6 +38,7 @@ export class VideoDownloadComponent { | |||
39 | 38 | ||
40 | download () { | 39 | download () { |
41 | window.location.assign(this.getLink()) | 40 | window.location.assign(this.getLink()) |
41 | this.activeModal.close() | ||
42 | } | 42 | } |
43 | 43 | ||
44 | getLink () { | 44 | getLink () { |
@@ -57,9 +57,6 @@ export class VideoDownloadComponent { | |||
57 | 57 | ||
58 | case 'torrent': | 58 | case 'torrent': |
59 | return file.torrentDownloadUrl | 59 | return file.torrentDownloadUrl |
60 | |||
61 | case 'magnet': | ||
62 | return file.magnetUri | ||
63 | } | 60 | } |
64 | } | 61 | } |
65 | 62 | ||
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 22f024656..e4d443a06 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' | 1 | import { VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' |
2 | import { AuthUser } from '../../core' | ||
3 | import { Video } from '../../shared/video/video.model' | 2 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 3 | import { Account } from '@app/shared/account/account.model' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 4 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 1f633d427..67d8e7711 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts | |||
@@ -85,6 +85,11 @@ export class VideoEdit implements VideoUpdate { | |||
85 | const originallyPublishedAt = new Date(values['originallyPublishedAt']) | 85 | const originallyPublishedAt = new Date(values['originallyPublishedAt']) |
86 | this.originallyPublishedAt = originallyPublishedAt.toISOString() | 86 | this.originallyPublishedAt = originallyPublishedAt.toISOString() |
87 | } | 87 | } |
88 | |||
89 | // Use the same file than the preview for the thumbnail | ||
90 | if (this.previewfile) { | ||
91 | this.thumbnailfile = this.previewfile | ||
92 | } | ||
88 | } | 93 | } |
89 | 94 | ||
90 | toFormPatch () { | 95 | toFormPatch () { |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 0cef3eb8f..6f9de9241 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -52,7 +52,6 @@ export class Video implements VideoServerModel { | |||
52 | 52 | ||
53 | account: { | 53 | account: { |
54 | id: number | 54 | id: number |
55 | uuid: string | ||
56 | name: string | 55 | name: string |
57 | displayName: string | 56 | displayName: string |
58 | url: string | 57 | url: string |
@@ -62,7 +61,6 @@ export class Video implements VideoServerModel { | |||
62 | 61 | ||
63 | channel: { | 62 | channel: { |
64 | id: number | 63 | id: number |
65 | uuid: string | ||
66 | name: string | 64 | name: string |
67 | displayName: string | 65 | displayName: string |
68 | url: string | 66 | url: string |
diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts index 955ebca9f..d69f7b70e 100644 --- a/client/src/app/shared/video/videos-selection.component.ts +++ b/client/src/app/shared/video/videos-selection.component.ts | |||
@@ -20,6 +20,7 @@ import { Video } from '@app/shared/video/video.model' | |||
20 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' | 20 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' |
21 | import { VideoSortField } from '@app/shared/video/sort-field.type' | 21 | import { VideoSortField } from '@app/shared/video/sort-field.type' |
22 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 22 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
23 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
23 | 24 | ||
24 | export type SelectionType = { [ id: number ]: boolean } | 25 | export type SelectionType = { [ id: number ]: boolean } |
25 | 26 | ||
@@ -44,6 +45,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni | |||
44 | globalButtonsTemplate: TemplateRef<any> | 45 | globalButtonsTemplate: TemplateRef<any> |
45 | 46 | ||
46 | constructor ( | 47 | constructor ( |
48 | protected i18n: I18n, | ||
47 | protected router: Router, | 49 | protected router: Router, |
48 | protected route: ActivatedRoute, | 50 | protected route: ActivatedRoute, |
49 | protected notifier: Notifier, | 51 | protected notifier: Notifier, |
diff --git a/client/src/app/signup/index.ts b/client/src/app/signup/index.ts deleted file mode 100644 index b0aca9723..000000000 --- a/client/src/app/signup/index.ts +++ /dev/null | |||
@@ -1,3 +0,0 @@ | |||
1 | export * from './signup-routing.module' | ||
2 | export * from './signup.component' | ||
3 | export * from './signup.module' | ||
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html deleted file mode 100644 index 07d24b381..000000000 --- a/client/src/app/signup/signup.component.html +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | <div class="margin-content"> | ||
2 | |||
3 | <div i18n class="title-page title-page-single"> | ||
4 | Create an account | ||
5 | </div> | ||
6 | |||
7 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> | ||
8 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
9 | |||
10 | <div class="d-flex justify-content-left flex-wrap"> | ||
11 | <form role="form" (ngSubmit)="signup()" [formGroup]="form"> | ||
12 | <div class="form-group"> | ||
13 | <label for="username" i18n>Username</label> | ||
14 | |||
15 | <div class="input-group"> | ||
16 | <input | ||
17 | type="text" id="username" i18n-placeholder placeholder="Example: jane_doe" | ||
18 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
19 | > | ||
20 | <div class="input-group-append"> | ||
21 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
22 | </div> | ||
23 | </div> | ||
24 | |||
25 | <div *ngIf="formErrors.username" class="form-error"> | ||
26 | {{ formErrors.username }} | ||
27 | </div> | ||
28 | </div> | ||
29 | |||
30 | <div class="form-group"> | ||
31 | <label for="email" i18n>Email</label> | ||
32 | <input | ||
33 | type="text" id="email" i18n-placeholder placeholder="Email" | ||
34 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
35 | > | ||
36 | <div *ngIf="formErrors.email" class="form-error"> | ||
37 | {{ formErrors.email }} | ||
38 | </div> | ||
39 | </div> | ||
40 | |||
41 | <div class="form-group"> | ||
42 | <label for="password" i18n>Password</label> | ||
43 | <input | ||
44 | type="password" id="password" i18n-placeholder placeholder="Password" | ||
45 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
46 | > | ||
47 | <div *ngIf="formErrors.password" class="form-error"> | ||
48 | {{ formErrors.password }} | ||
49 | </div> | ||
50 | </div> | ||
51 | |||
52 | <div class="form-group form-group-terms"> | ||
53 | <my-peertube-checkbox | ||
54 | inputName="terms" formControlName="terms" | ||
55 | i18n-labelHtml labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | ||
56 | ></my-peertube-checkbox> | ||
57 | |||
58 | <div *ngIf="formErrors.terms" class="form-error"> | ||
59 | {{ formErrors.terms }} | ||
60 | </div> | ||
61 | </div> | ||
62 | |||
63 | <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone"> | ||
64 | </form> | ||
65 | |||
66 | <div> | ||
67 | <label i18n>Features found on this instance</label> | ||
68 | <my-instance-features-table></my-instance-features-table> | ||
69 | </div> | ||
70 | </div> | ||
71 | |||
72 | </div> | ||
diff --git a/client/src/app/signup/signup.component.scss b/client/src/app/signup/signup.component.scss deleted file mode 100644 index 90e1e8e74..000000000 --- a/client/src/app/signup/signup.component.scss +++ /dev/null | |||
@@ -1,39 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-instance-features-table { | ||
5 | display: block; | ||
6 | |||
7 | margin-bottom: 40px; | ||
8 | } | ||
9 | |||
10 | form { | ||
11 | margin: 0 60px 40px 0; | ||
12 | } | ||
13 | |||
14 | .form-group-terms { | ||
15 | margin: 30px 0; | ||
16 | } | ||
17 | |||
18 | .input-group { | ||
19 | @include peertube-input-group(400px); | ||
20 | } | ||
21 | |||
22 | .input-group-append { | ||
23 | height: 30px; | ||
24 | } | ||
25 | |||
26 | input:not([type=submit]) { | ||
27 | @include peertube-input-text(400px); | ||
28 | display: block; | ||
29 | |||
30 | &#username { | ||
31 | width: auto; | ||
32 | flex-grow: 1; | ||
33 | } | ||
34 | } | ||
35 | |||
36 | input[type=submit] { | ||
37 | @include peertube-button; | ||
38 | @include orange-button; | ||
39 | } | ||
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts deleted file mode 100644 index 13941ec79..000000000 --- a/client/src/app/signup/signup.component.ts +++ /dev/null | |||
@@ -1,78 +0,0 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' | ||
3 | import { UserCreate } from '../../../../shared' | ||
4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-signup', | ||
10 | templateUrl: './signup.component.html', | ||
11 | styleUrls: [ './signup.component.scss' ] | ||
12 | }) | ||
13 | export class SignupComponent extends FormReactive implements OnInit { | ||
14 | info: string = null | ||
15 | error: string = null | ||
16 | signupDone = false | ||
17 | |||
18 | constructor ( | ||
19 | protected formValidatorService: FormValidatorService, | ||
20 | private authService: AuthService, | ||
21 | private userValidatorsService: UserValidatorsService, | ||
22 | private notifier: Notifier, | ||
23 | private userService: UserService, | ||
24 | private serverService: ServerService, | ||
25 | private redirectService: RedirectService, | ||
26 | private i18n: I18n | ||
27 | ) { | ||
28 | super() | ||
29 | } | ||
30 | |||
31 | get instanceHost () { | ||
32 | return window.location.host | ||
33 | } | ||
34 | |||
35 | get requiresEmailVerification () { | ||
36 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
37 | } | ||
38 | |||
39 | ngOnInit () { | ||
40 | this.buildForm({ | ||
41 | username: this.userValidatorsService.USER_USERNAME, | ||
42 | password: this.userValidatorsService.USER_PASSWORD, | ||
43 | email: this.userValidatorsService.USER_EMAIL, | ||
44 | terms: this.userValidatorsService.USER_TERMS | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | signup () { | ||
49 | this.error = null | ||
50 | |||
51 | const userCreate: UserCreate = this.form.value | ||
52 | |||
53 | this.userService.signup(userCreate).subscribe( | ||
54 | () => { | ||
55 | this.signupDone = true | ||
56 | |||
57 | if (this.requiresEmailVerification) { | ||
58 | this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.') | ||
59 | return | ||
60 | } | ||
61 | |||
62 | // Auto login | ||
63 | this.authService.login(userCreate.username, userCreate.password) | ||
64 | .subscribe( | ||
65 | () => { | ||
66 | this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })) | ||
67 | |||
68 | this.redirectService.redirectToHomepage() | ||
69 | }, | ||
70 | |||
71 | err => this.error = err.message | ||
72 | ) | ||
73 | }, | ||
74 | |||
75 | err => this.error = err.message | ||
76 | ) | ||
77 | } | ||
78 | } | ||
diff --git a/client/src/app/signup/signup.module.ts b/client/src/app/signup/signup.module.ts deleted file mode 100644 index 61560ddcf..000000000 --- a/client/src/app/signup/signup.module.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | |||
3 | import { SignupRoutingModule } from './signup-routing.module' | ||
4 | import { SignupComponent } from './signup.component' | ||
5 | import { SharedModule } from '../shared' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | SignupRoutingModule, | ||
10 | SharedModule | ||
11 | ], | ||
12 | |||
13 | declarations: [ | ||
14 | SignupComponent | ||
15 | ], | ||
16 | |||
17 | exports: [ | ||
18 | SignupComponent | ||
19 | ], | ||
20 | |||
21 | providers: [ | ||
22 | ] | ||
23 | }) | ||
24 | export class SignupModule { } | ||
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 99695204d..28572d611 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -187,18 +187,14 @@ | |||
187 | <ng-template ngbTabContent> | 187 | <ng-template ngbTabContent> |
188 | <div class="row advanced-settings"> | 188 | <div class="row advanced-settings"> |
189 | <div class="col-md-12 col-xl-8"> | 189 | <div class="col-md-12 col-xl-8"> |
190 | <div class="form-group"> | ||
191 | <my-image-upload | ||
192 | i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile" | ||
193 | previewWidth="200px" previewHeight="110px" | ||
194 | ></my-image-upload> | ||
195 | </div> | ||
196 | 190 | ||
197 | <div class="form-group"> | 191 | <div class="form-group"> |
198 | <my-image-upload | 192 | <label i18n for="previewfile">Video preview</label> |
199 | i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile" | 193 | |
194 | <my-preview-upload | ||
195 | i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile" | ||
200 | previewWidth="360px" previewHeight="200px" | 196 | previewWidth="360px" previewHeight="200px" |
201 | ></my-image-upload> | 197 | ></my-preview-upload> |
202 | </div> | 198 | </div> |
203 | 199 | ||
204 | <div class="form-group"> | 200 | <div class="form-group"> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 8345645f6..cea352bfb 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts | |||
@@ -13,6 +13,7 @@ import { VideoCaptionAddModalComponent } from '@app/videos/+video-edit/shared/vi | |||
13 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | 13 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' |
14 | import { removeElementFromArray } from '@app/shared/misc/utils' | 14 | import { removeElementFromArray } from '@app/shared/misc/utils' |
15 | import { VideoConstant, VideoPrivacy } from '../../../../../../shared' | 15 | import { VideoConstant, VideoPrivacy } from '../../../../../../shared' |
16 | import { VideoService } from '@app/shared/video/video.service' | ||
16 | 17 | ||
17 | @Component({ | 18 | @Component({ |
18 | selector: 'my-video-edit', | 19 | selector: 'my-video-edit', |
@@ -23,7 +24,6 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
23 | @Input() form: FormGroup | 24 | @Input() form: FormGroup |
24 | @Input() formErrors: { [ id: string ]: string } = {} | 25 | @Input() formErrors: { [ id: string ]: string } = {} |
25 | @Input() validationMessages: FormReactiveValidationMessages = {} | 26 | @Input() validationMessages: FormReactiveValidationMessages = {} |
26 | @Input() videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | ||
27 | @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] | 27 | @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] |
28 | @Input() schedulePublicationPossible = true | 28 | @Input() schedulePublicationPossible = true |
29 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] | 29 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] |
@@ -34,6 +34,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
34 | // So that it can be accessed in the template | 34 | // So that it can be accessed in the template |
35 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | 35 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY |
36 | 36 | ||
37 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | ||
37 | videoCategories: VideoConstant<number>[] = [] | 38 | videoCategories: VideoConstant<number>[] = [] |
38 | videoLicences: VideoConstant<number>[] = [] | 39 | videoLicences: VideoConstant<number>[] = [] |
39 | videoLanguages: VideoConstant<string>[] = [] | 40 | videoLanguages: VideoConstant<string>[] = [] |
@@ -58,6 +59,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
58 | private formValidatorService: FormValidatorService, | 59 | private formValidatorService: FormValidatorService, |
59 | private videoValidatorsService: VideoValidatorsService, | 60 | private videoValidatorsService: VideoValidatorsService, |
60 | private videoCaptionService: VideoCaptionService, | 61 | private videoCaptionService: VideoCaptionService, |
62 | private videoService: VideoService, | ||
61 | private route: ActivatedRoute, | 63 | private route: ActivatedRoute, |
62 | private router: Router, | 64 | private router: Router, |
63 | private notifier: Notifier, | 65 | private notifier: Notifier, |
@@ -100,7 +102,6 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
100 | language: this.videoValidatorsService.VIDEO_LANGUAGE, | 102 | language: this.videoValidatorsService.VIDEO_LANGUAGE, |
101 | description: this.videoValidatorsService.VIDEO_DESCRIPTION, | 103 | description: this.videoValidatorsService.VIDEO_DESCRIPTION, |
102 | tags: null, | 104 | tags: null, |
103 | thumbnailfile: null, | ||
104 | previewfile: null, | 105 | previewfile: null, |
105 | support: this.videoValidatorsService.VIDEO_SUPPORT, | 106 | support: this.videoValidatorsService.VIDEO_SUPPORT, |
106 | schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, | 107 | schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, |
@@ -133,6 +134,9 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
133 | this.videoLicences = this.serverService.getVideoLicences() | 134 | this.videoLicences = this.serverService.getVideoLicences() |
134 | this.videoLanguages = this.serverService.getVideoLanguages() | 135 | this.videoLanguages = this.serverService.getVideoLanguages() |
135 | 136 | ||
137 | const privacies = this.serverService.getVideoPrivacies() | ||
138 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies) | ||
139 | |||
136 | this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id) | 140 | this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id) |
137 | 141 | ||
138 | this.ngZone.runOutsideAngular(() => { | 142 | this.ngZone.runOutsideAngular(() => { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html index 537d7ffa2..7a495fea5 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html | |||
@@ -58,7 +58,7 @@ | |||
58 | <form [hidden]="!hasImportedVideo" novalidate [formGroup]="form"> | 58 | <form [hidden]="!hasImportedVideo" novalidate [formGroup]="form"> |
59 | <my-video-edit | 59 | <my-video-edit |
60 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" | 60 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" |
61 | [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" | 61 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" |
62 | ></my-video-edit> | 62 | ></my-video-edit> |
63 | 63 | ||
64 | <div class="submit-container"> | 64 | <div class="submit-container"> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts index d2e9f6cfe..e47624dd6 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts | |||
@@ -100,8 +100,6 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
100 | previewUrl: null | 100 | previewUrl: null |
101 | })) | 101 | })) |
102 | 102 | ||
103 | this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) | ||
104 | |||
105 | this.hydrateFormFromVideo() | 103 | this.hydrateFormFromVideo() |
106 | }, | 104 | }, |
107 | 105 | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html index 984b9d590..e4f19faa8 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html | |||
@@ -51,7 +51,7 @@ | |||
51 | <form [hidden]="!hasImportedVideo" novalidate [formGroup]="form"> | 51 | <form [hidden]="!hasImportedVideo" novalidate [formGroup]="form"> |
52 | <my-video-edit | 52 | <my-video-edit |
53 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" | 53 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" |
54 | [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" | 54 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" |
55 | ></my-video-edit> | 55 | ></my-video-edit> |
56 | 56 | ||
57 | <div class="submit-container"> | 57 | <div class="submit-container"> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts index 2dffdbf0e..a5578bebd 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts | |||
@@ -91,8 +91,6 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
91 | previewUrl: null | 91 | previewUrl: null |
92 | })) | 92 | })) |
93 | 93 | ||
94 | this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) | ||
95 | |||
96 | this.hydrateFormFromVideo() | 94 | this.hydrateFormFromVideo() |
97 | }, | 95 | }, |
98 | 96 | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts index 8401caeec..580c123a0 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts | |||
@@ -14,7 +14,6 @@ import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate- | |||
14 | export abstract class VideoSend extends FormReactive implements OnInit { | 14 | export abstract class VideoSend extends FormReactive implements OnInit { |
15 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 15 | userVideoChannels: { id: number, label: string, support: string }[] = [] |
16 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 16 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] |
17 | explainedVideoPrivacies: VideoConstant<VideoPrivacy>[] = [] | ||
18 | videoCaptions: VideoCaptionEdit[] = [] | 17 | videoCaptions: VideoCaptionEdit[] = [] |
19 | 18 | ||
20 | firstStepPrivacyId = 0 | 19 | firstStepPrivacyId = 0 |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html index 536769d2f..0f904affb 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -26,6 +26,27 @@ | |||
26 | </select> | 26 | </select> |
27 | </div> | 27 | </div> |
28 | </div> | 28 | </div> |
29 | |||
30 | <ng-container *ngIf="isUploadingAudioFile"> | ||
31 | <div class="form-group audio-preview"> | ||
32 | <label i18n for="previewfileUpload">Video background image</label> | ||
33 | |||
34 | <div i18n class="audio-image-info"> | ||
35 | Image that will be merged with your audio file. | ||
36 | <br /> | ||
37 | The chosen image will be definitive and cannot be modified. | ||
38 | </div> | ||
39 | |||
40 | <my-preview-upload | ||
41 | i18n-inputLabel inputLabel="Edit" inputName="previewfileUpload" [(ngModel)]="previewfileUpload" | ||
42 | previewWidth="360px" previewHeight="200px" | ||
43 | ></my-preview-upload> | ||
44 | </div> | ||
45 | |||
46 | <div class="form-group upload-audio-button"> | ||
47 | <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button> | ||
48 | </div> | ||
49 | </ng-container> | ||
29 | </div> | 50 | </div> |
30 | </div> | 51 | </div> |
31 | 52 | ||
@@ -50,7 +71,7 @@ | |||
50 | <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> | 71 | <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> |
51 | <my-video-edit | 72 | <my-video-edit |
52 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" | 73 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" |
53 | [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" | 74 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" |
54 | [waitTranscodingEnabled]="waitTranscodingEnabled" | 75 | [waitTranscodingEnabled]="waitTranscodingEnabled" |
55 | ></my-video-edit> | 76 | ></my-video-edit> |
56 | 77 | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss index 8adf8f169..684342f09 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss | |||
@@ -1,9 +1,20 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .first-step-block .form-group-channel { | 4 | .first-step-block { |
5 | margin-bottom: 20px; | 5 | |
6 | margin-top: 35px; | 6 | .form-group-channel { |
7 | margin-bottom: 20px; | ||
8 | margin-top: 35px; | ||
9 | } | ||
10 | |||
11 | .audio-image-info { | ||
12 | margin-bottom: 10px; | ||
13 | } | ||
14 | |||
15 | .audio-preview { | ||
16 | margin: 30px 0; | ||
17 | } | ||
7 | } | 18 | } |
8 | 19 | ||
9 | .upload-progress-cancel { | 20 | .upload-progress-cancel { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts index d6d4bad21..69fa13a2f 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -35,8 +35,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
35 | userVideoQuotaUsed = 0 | 35 | userVideoQuotaUsed = 0 |
36 | userVideoQuotaUsedDaily = 0 | 36 | userVideoQuotaUsedDaily = 0 |
37 | 37 | ||
38 | isUploadingAudioFile = false | ||
38 | isUploadingVideo = false | 39 | isUploadingVideo = false |
39 | isUpdatingVideo = false | 40 | isUpdatingVideo = false |
41 | |||
40 | videoUploaded = false | 42 | videoUploaded = false |
41 | videoUploadObservable: Subscription = null | 43 | videoUploadObservable: Subscription = null |
42 | videoUploadPercents = 0 | 44 | videoUploadPercents = 0 |
@@ -44,7 +46,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
44 | id: 0, | 46 | id: 0, |
45 | uuid: '' | 47 | uuid: '' |
46 | } | 48 | } |
49 | |||
47 | waitTranscodingEnabled = true | 50 | waitTranscodingEnabled = true |
51 | previewfileUpload: File | ||
48 | 52 | ||
49 | error: string | 53 | error: string |
50 | 54 | ||
@@ -100,6 +104,17 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
100 | } | 104 | } |
101 | } | 105 | } |
102 | 106 | ||
107 | getVideoFile () { | ||
108 | return this.videofileInput.nativeElement.files[0] | ||
109 | } | ||
110 | |||
111 | getAudioUploadLabel () { | ||
112 | const videofile = this.getVideoFile() | ||
113 | if (!videofile) return this.i18n('Upload') | ||
114 | |||
115 | return this.i18n('Upload {{videofileName}}', { videofileName: videofile.name }) | ||
116 | } | ||
117 | |||
103 | fileChange () { | 118 | fileChange () { |
104 | this.uploadFirstStep() | 119 | this.uploadFirstStep() |
105 | } | 120 | } |
@@ -114,38 +129,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
114 | } | 129 | } |
115 | } | 130 | } |
116 | 131 | ||
117 | uploadFirstStep () { | 132 | uploadFirstStep (clickedOnButton = false) { |
118 | const videofile = this.videofileInput.nativeElement.files[0] | 133 | const videofile = this.getVideoFile() |
119 | if (!videofile) return | 134 | if (!videofile) return |
120 | 135 | ||
121 | // Check global user quota | 136 | if (!this.checkGlobalUserQuota(videofile)) return |
122 | const bytePipes = new BytesPipe() | 137 | if (!this.checkDailyUserQuota(videofile)) return |
123 | const videoQuota = this.authService.getUser().videoQuota | ||
124 | if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { | ||
125 | const msg = this.i18n( | ||
126 | 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})', | ||
127 | { | ||
128 | videoSize: bytePipes.transform(videofile.size, 0), | ||
129 | videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0), | ||
130 | videoQuota: bytePipes.transform(videoQuota, 0) | ||
131 | } | ||
132 | ) | ||
133 | this.notifier.error(msg) | ||
134 | return | ||
135 | } | ||
136 | 138 | ||
137 | // Check daily user quota | 139 | if (clickedOnButton === false && this.isAudioFile(videofile.name)) { |
138 | const videoQuotaDaily = this.authService.getUser().videoQuotaDaily | 140 | this.isUploadingAudioFile = true |
139 | if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { | ||
140 | const msg = this.i18n( | ||
141 | 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})', | ||
142 | { | ||
143 | videoSize: bytePipes.transform(videofile.size, 0), | ||
144 | quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0), | ||
145 | quotaDaily: bytePipes.transform(videoQuotaDaily, 0) | ||
146 | } | ||
147 | ) | ||
148 | this.notifier.error(msg) | ||
149 | return | 141 | return |
150 | } | 142 | } |
151 | 143 | ||
@@ -180,6 +172,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
180 | formData.append('channelId', '' + channelId) | 172 | formData.append('channelId', '' + channelId) |
181 | formData.append('videofile', videofile) | 173 | formData.append('videofile', videofile) |
182 | 174 | ||
175 | if (this.previewfileUpload) { | ||
176 | formData.append('previewfile', this.previewfileUpload) | ||
177 | formData.append('thumbnailfile', this.previewfileUpload) | ||
178 | } | ||
179 | |||
183 | this.isUploadingVideo = true | 180 | this.isUploadingVideo = true |
184 | this.firstStepDone.emit(name) | 181 | this.firstStepDone.emit(name) |
185 | 182 | ||
@@ -187,11 +184,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
187 | name, | 184 | name, |
188 | privacy, | 185 | privacy, |
189 | nsfw, | 186 | nsfw, |
190 | channelId | 187 | channelId, |
188 | previewfile: this.previewfileUpload | ||
191 | }) | 189 | }) |
192 | 190 | ||
193 | this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) | ||
194 | |||
195 | this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( | 191 | this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( |
196 | event => { | 192 | event => { |
197 | if (event.type === HttpEventType.UploadProgress) { | 193 | if (event.type === HttpEventType.UploadProgress) { |
@@ -251,4 +247,52 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
251 | } | 247 | } |
252 | ) | 248 | ) |
253 | } | 249 | } |
250 | |||
251 | private checkGlobalUserQuota (videofile: File) { | ||
252 | const bytePipes = new BytesPipe() | ||
253 | |||
254 | // Check global user quota | ||
255 | const videoQuota = this.authService.getUser().videoQuota | ||
256 | if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { | ||
257 | const msg = this.i18n( | ||
258 | 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})', | ||
259 | { | ||
260 | videoSize: bytePipes.transform(videofile.size, 0), | ||
261 | videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0), | ||
262 | videoQuota: bytePipes.transform(videoQuota, 0) | ||
263 | } | ||
264 | ) | ||
265 | this.notifier.error(msg) | ||
266 | |||
267 | return false | ||
268 | } | ||
269 | |||
270 | return true | ||
271 | } | ||
272 | |||
273 | private checkDailyUserQuota (videofile: File) { | ||
274 | const bytePipes = new BytesPipe() | ||
275 | |||
276 | // Check daily user quota | ||
277 | const videoQuotaDaily = this.authService.getUser().videoQuotaDaily | ||
278 | if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { | ||
279 | const msg = this.i18n( | ||
280 | 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})', | ||
281 | { | ||
282 | videoSize: bytePipes.transform(videofile.size, 0), | ||
283 | quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0), | ||
284 | quotaDaily: bytePipes.transform(videoQuotaDaily, 0) | ||
285 | } | ||
286 | ) | ||
287 | this.notifier.error(msg) | ||
288 | |||
289 | return false | ||
290 | } | ||
291 | |||
292 | return true | ||
293 | } | ||
294 | |||
295 | private isAudioFile (filename: string) { | ||
296 | return filename.endsWith('.mp3') || filename.endsWith('.flac') || filename.endsWith('.ogg') | ||
297 | } | ||
254 | } | 298 | } |
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index b5cab7ed5..aa148311f 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html | |||
@@ -7,7 +7,7 @@ | |||
7 | 7 | ||
8 | <my-video-edit | 8 | <my-video-edit |
9 | [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" | 9 | [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" |
10 | [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" | 10 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" |
11 | [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" | 11 | [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" |
12 | ></my-video-edit> | 12 | ></my-video-edit> |
13 | 13 | ||
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 10f797d02..81c66ff20 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts | |||
@@ -3,7 +3,6 @@ import { Component, HostListener, OnInit } from '@angular/core' | |||
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { LoadingBarService } from '@ngx-loading-bar/core' | 4 | import { LoadingBarService } from '@ngx-loading-bar/core' |
5 | import { Notifier } from '@app/core' | 5 | import { Notifier } from '@app/core' |
6 | import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' | ||
7 | import { ServerService } from '../../core' | 6 | import { ServerService } from '../../core' |
8 | import { FormReactive } from '../../shared' | 7 | import { FormReactive } from '../../shared' |
9 | import { VideoEdit } from '../../shared/video/video-edit.model' | 8 | import { VideoEdit } from '../../shared/video/video-edit.model' |
@@ -13,6 +12,7 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val | |||
13 | import { VideoCaptionService } from '@app/shared/video-caption' | 12 | import { VideoCaptionService } from '@app/shared/video-caption' |
14 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | 13 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' |
15 | import { VideoDetails } from '@app/shared/video/video-details.model' | 14 | import { VideoDetails } from '@app/shared/video/video-details.model' |
15 | import { VideoPrivacy } from '@shared/models' | ||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-videos-update', | 18 | selector: 'my-videos-update', |
@@ -23,8 +23,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
23 | video: VideoEdit | 23 | video: VideoEdit |
24 | 24 | ||
25 | isUpdatingVideo = false | 25 | isUpdatingVideo = false |
26 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | ||
27 | explainedVideoPrivacies: VideoConstant<VideoPrivacy>[] = [] | ||
28 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 26 | userVideoChannels: { id: number, label: string, support: string }[] = [] |
29 | schedulePublicationPossible = false | 27 | schedulePublicationPossible = false |
30 | videoCaptions: VideoCaptionEdit[] = [] | 28 | videoCaptions: VideoCaptionEdit[] = [] |
@@ -49,9 +47,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
49 | ngOnInit () { | 47 | ngOnInit () { |
50 | this.buildForm({}) | 48 | this.buildForm({}) |
51 | 49 | ||
52 | this.serverService.videoPrivaciesLoaded | ||
53 | .subscribe(() => this.videoPrivacies = this.serverService.getVideoPrivacies()) | ||
54 | |||
55 | this.route.data | 50 | this.route.data |
56 | .pipe(map(data => data.videoData)) | 51 | .pipe(map(data => data.videoData)) |
57 | .subscribe(({ video, videoChannels, videoCaptions }) => { | 52 | .subscribe(({ video, videoChannels, videoCaptions }) => { |
@@ -59,14 +54,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
59 | this.userVideoChannels = videoChannels | 54 | this.userVideoChannels = videoChannels |
60 | this.videoCaptions = videoCaptions | 55 | this.videoCaptions = videoCaptions |
61 | 56 | ||
62 | // We cannot set private a video that was not private | 57 | this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE |
63 | if (this.video.privacy !== VideoPrivacy.PRIVATE) { | ||
64 | this.videoPrivacies = this.videoPrivacies.filter(p => p.id !== VideoPrivacy.PRIVATE) | ||
65 | } else { // We can schedule video publication only if it it is private | ||
66 | this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE | ||
67 | } | ||
68 | |||
69 | this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) | ||
70 | 58 | ||
71 | const videoFiles = (video as VideoDetails).files | 59 | const videoFiles = (video as VideoDetails).files |
72 | if (videoFiles.length > 1) { // Already transcoded | 60 | if (videoFiles.length > 1) { // Already transcoded |
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html index 955b2b80c..82e59d04d 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.html +++ b/client/src/app/videos/+video-watch/modal/video-share.component.html | |||
@@ -5,53 +5,167 @@ | |||
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
8 | <ngb-tabset class="root-tabset bootstrap" (tabChange)="onTabChange($event)"> | ||
8 | 9 | ||
9 | <div class="start-at"> | 10 | <ngb-tab i18n-title title="URL" id="url"> |
10 | <my-peertube-checkbox | 11 | <ng-template ngbTabContent> |
11 | inputName="startAt" [(ngModel)]="startAtCheckbox" | 12 | |
12 | i18n-labelText labelText="Start at" | 13 | <div class="tab-content"> |
13 | ></my-peertube-checkbox> | 14 | <div class="input-group"> |
14 | 15 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="getVideoUrl()" /> | |
15 | <my-timestamp-input | 16 | <div class="input-group-append"> |
16 | [timestamp]="currentVideoTimestamp" | 17 | <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> |
17 | [maxTimestamp]="video.duration" | 18 | <span class="glyphicon glyphicon-copy"></span> |
18 | [disabled]="!startAtCheckbox" | 19 | </button> |
19 | [(ngModel)]="currentVideoTimestamp" | 20 | </div> |
20 | > | 21 | </div> |
21 | </my-timestamp-input> | 22 | </div> |
22 | </div> | 23 | |
24 | </ng-template> | ||
25 | </ngb-tab> | ||
26 | |||
27 | <ngb-tab i18n-title title="QR-Code" id="qrcode"> | ||
28 | <ng-template ngbTabContent> | ||
29 | <div class="tab-content"> | ||
30 | <ngx-qrcode qrc-element-type="url" [qrc-value]="getVideoUrl()" qrc-errorCorrectionLevel="Q"></ngx-qrcode> | ||
31 | </div> | ||
32 | </ng-template> | ||
33 | </ngb-tab> | ||
34 | |||
35 | <ngb-tab i18n-title title="Embed" id="embed"> | ||
36 | <ng-template ngbTabContent> | ||
37 | <div class="tab-content"> | ||
38 | <div class="input-group"> | ||
39 | <input #shareInput (click)="shareInput.select()" type="text" class="form-control readonly" readonly [value]="getVideoIframeCode()" /> | ||
40 | <div class="input-group-append"> | ||
41 | <button [ngxClipboard]="shareInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | ||
42 | <span class="glyphicon glyphicon-copy"></span> | ||
43 | </button> | ||
44 | </div> | ||
45 | </div> | ||
46 | |||
47 | <div i18n *ngIf="notSecure()" class="alert alert-warning"> | ||
48 | The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites). | ||
49 | </div> | ||
50 | </div> | ||
51 | </ng-template> | ||
52 | </ngb-tab> | ||
53 | |||
54 | </ngb-tabset> | ||
23 | 55 | ||
24 | <div class="form-group"> | 56 | <div class="filters"> |
25 | <label i18n>URL</label> | 57 | <div> |
26 | <div class="input-group input-group-sm"> | 58 | <div class="form-group start-at"> |
27 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoUrl()" /> | 59 | <my-peertube-checkbox |
28 | <div class="input-group-append"> | 60 | inputName="startAt" [(ngModel)]="customizations.startAtCheckbox" |
29 | <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | 61 | i18n-labelText labelText="Start at" |
30 | <span class="glyphicon glyphicon-copy"></span> | 62 | ></my-peertube-checkbox> |
31 | </button> | 63 | |
64 | <my-timestamp-input | ||
65 | [timestamp]="customizations.startAt" | ||
66 | [maxTimestamp]="video.duration" | ||
67 | [disabled]="!customizations.startAtCheckbox" | ||
68 | [(ngModel)]="customizations.startAt" | ||
69 | > | ||
70 | </my-timestamp-input> | ||
32 | </div> | 71 | </div> |
33 | </div> | ||
34 | </div> | ||
35 | 72 | ||
36 | <div class="form-group qr-code-group"> | 73 | <div *ngIf="videoCaptions.length !== 0" class="form-group video-caption-block"> |
37 | <label i18n>QR-Code</label> | 74 | <my-peertube-checkbox |
38 | <ngx-qrcode qrc-element-type="url" [qrc-value]="getVideoUrl()" qrc-errorCorrectionLevel="Q"></ngx-qrcode> | 75 | inputName="subtitleCheckbox" [(ngModel)]="customizations.subtitleCheckbox" |
39 | </div> | 76 | i18n-labelText labelText="Auto select subtitle" |
77 | ></my-peertube-checkbox> | ||
40 | 78 | ||
41 | <div class="form-group"> | 79 | <div class="peertube-select-container" [ngClass]="{ disabled: !customizations.subtitleCheckbox }"> |
42 | <label i18n>Embed</label> | 80 | <select [(ngModel)]="customizations.subtitle" [disabled]="!customizations.subtitleCheckbox"> |
43 | <div class="input-group input-group-sm"> | 81 | <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option> |
44 | <input #shareInput (click)="shareInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoIframeCode()" /> | 82 | </select> |
45 | <div class="input-group-append"> | 83 | </div> |
46 | <button [ngxClipboard]="shareInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | ||
47 | <span class="glyphicon glyphicon-copy"></span> | ||
48 | </button> | ||
49 | </div> | 84 | </div> |
50 | </div> | 85 | </div> |
51 | </div> | ||
52 | 86 | ||
53 | <div i18n *ngIf="notSecure()" class="alert alert-warning"> | 87 | <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button" |
54 | The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites). | 88 | [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic"> |
89 | |||
90 | <ng-container *ngIf="isAdvancedCustomizationCollapsed"> | ||
91 | <span class="glyphicon glyphicon-menu-down"></span> | ||
92 | |||
93 | <ng-container i18n> | ||
94 | More customization | ||
95 | </ng-container> | ||
96 | </ng-container> | ||
97 | |||
98 | <ng-container *ngIf="!isAdvancedCustomizationCollapsed"> | ||
99 | <span class="glyphicon glyphicon-menu-up"></span> | ||
100 | |||
101 | <ng-container i18n> | ||
102 | Less customization | ||
103 | </ng-container> | ||
104 | </ng-container> | ||
105 | </div> | ||
106 | |||
107 | <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed"> | ||
108 | <div> | ||
109 | <div class="form-group stop-at"> | ||
110 | <my-peertube-checkbox | ||
111 | inputName="stopAt" [(ngModel)]="customizations.stopAtCheckbox" | ||
112 | i18n-labelText labelText="Stop at" | ||
113 | ></my-peertube-checkbox> | ||
114 | |||
115 | <my-timestamp-input | ||
116 | [timestamp]="customizations.stopAt" | ||
117 | [maxTimestamp]="video.duration" | ||
118 | [disabled]="!customizations.stopAtCheckbox" | ||
119 | [(ngModel)]="customizations.stopAt" | ||
120 | > | ||
121 | </my-timestamp-input> | ||
122 | </div> | ||
123 | |||
124 | <div class="form-group"> | ||
125 | <my-peertube-checkbox | ||
126 | inputName="autoplay" [(ngModel)]="customizations.autoplay" | ||
127 | i18n-labelText labelText="Autoplay" | ||
128 | ></my-peertube-checkbox> | ||
129 | </div> | ||
130 | |||
131 | <div class="form-group"> | ||
132 | <my-peertube-checkbox | ||
133 | inputName="muted" [(ngModel)]="customizations.muted" | ||
134 | i18n-labelText labelText="Muted" | ||
135 | ></my-peertube-checkbox> | ||
136 | </div> | ||
137 | |||
138 | <div class="form-group"> | ||
139 | <my-peertube-checkbox | ||
140 | inputName="loop" [(ngModel)]="customizations.loop" | ||
141 | i18n-labelText labelText="Loop" | ||
142 | ></my-peertube-checkbox> | ||
143 | </div> | ||
144 | </div> | ||
145 | |||
146 | <ng-container *ngIf="isInEmbedTab()"> | ||
147 | <div class="form-group"> | ||
148 | <my-peertube-checkbox | ||
149 | inputName="title" [(ngModel)]="customizations.title" | ||
150 | i18n-labelText labelText="Display video title" | ||
151 | ></my-peertube-checkbox> | ||
152 | </div> | ||
153 | |||
154 | <div class="form-group"> | ||
155 | <my-peertube-checkbox | ||
156 | inputName="warningTitle" [(ngModel)]="customizations.warningTitle" | ||
157 | i18n-labelText labelText="Display privacy warning" | ||
158 | ></my-peertube-checkbox> | ||
159 | </div> | ||
160 | |||
161 | <div class="form-group"> | ||
162 | <my-peertube-checkbox | ||
163 | inputName="controls" [(ngModel)]="customizations.controls" | ||
164 | i18n-labelText labelText="Display player controls" | ||
165 | ></my-peertube-checkbox> | ||
166 | </div> | ||
167 | </ng-container> | ||
168 | </div> | ||
55 | </div> | 169 | </div> |
56 | </div> | 170 | </div> |
57 | 171 | ||
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.scss b/client/src/app/videos/+video-watch/modal/video-share.component.scss index 472a45920..c48abf9e0 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.scss +++ b/client/src/app/videos/+video-watch/modal/video-share.component.scss | |||
@@ -1,5 +1,9 @@ | |||
1 | @import '~bootstrap/scss/functions'; | 1 | @import '_mixins'; |
2 | @import '~bootstrap/scss/variables'; | 2 | @import '_variables'; |
3 | |||
4 | .peertube-select-container { | ||
5 | @include peertube-select-container(200px); | ||
6 | } | ||
3 | 7 | ||
4 | .action-button-cancel { | 8 | .action-button-cancel { |
5 | margin-right: 0 !important; | 9 | margin-right: 0 !important; |
@@ -9,13 +13,65 @@ | |||
9 | text-align: center; | 13 | text-align: center; |
10 | } | 14 | } |
11 | 15 | ||
12 | .start-at { | 16 | .tab-content { |
17 | margin-top: 30px; | ||
13 | display: flex; | 18 | display: flex; |
14 | justify-content: center; | 19 | justify-content: center; |
15 | margin-top: 10px; | ||
16 | align-items: center; | 20 | align-items: center; |
21 | flex-direction: column; | ||
22 | } | ||
23 | |||
24 | .alert { | ||
25 | margin-top: 20px; | ||
26 | } | ||
27 | |||
28 | input.readonly { | ||
29 | font-size: 15px; | ||
30 | } | ||
31 | |||
32 | .filters { | ||
33 | margin-top: 30px; | ||
34 | padding-top: 30px; | ||
35 | border-top: 1px solid $separator-border-color; | ||
36 | |||
37 | .advanced-filters-button { | ||
38 | display: flex; | ||
39 | justify-content: center; | ||
40 | align-items: center; | ||
41 | margin-top: 30px; | ||
42 | font-size: 16px; | ||
43 | font-weight: $font-semibold; | ||
44 | cursor: pointer; | ||
45 | |||
46 | .glyphicon { | ||
47 | margin-right: 5px; | ||
48 | } | ||
49 | } | ||
50 | |||
51 | .form-group { | ||
52 | margin-bottom: 0; | ||
53 | height: 34px; | ||
54 | display: flex; | ||
55 | align-items: center; | ||
56 | } | ||
57 | |||
58 | .video-caption-block { | ||
59 | display: flex; | ||
60 | align-items: center; | ||
61 | |||
62 | .peertube-select-container { | ||
63 | margin-left: 10px; | ||
64 | } | ||
65 | } | ||
66 | |||
67 | .start-at, | ||
68 | .stop-at { | ||
69 | width: 300px; | ||
70 | display: flex; | ||
71 | align-items: center; | ||
17 | 72 | ||
18 | my-timestamp-input { | 73 | my-timestamp-input { |
19 | margin-left: 10px; | 74 | margin-left: 10px; |
75 | } | ||
20 | } | 76 | } |
21 | } | 77 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts index 6565d7f88..eaaf6b902 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts | |||
@@ -3,8 +3,26 @@ import { Notifier } from '@app/core' | |||
3 | import { VideoDetails } from '../../../shared/video/video-details.model' | 3 | import { VideoDetails } from '../../../shared/video/video-details.model' |
4 | import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' | 4 | import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap' |
7 | import { durationToString } from '@app/shared/misc/utils' | 7 | import { VideoCaption } from '@shared/models' |
8 | |||
9 | type Customizations = { | ||
10 | startAtCheckbox: boolean | ||
11 | startAt: number | ||
12 | |||
13 | stopAtCheckbox: boolean | ||
14 | stopAt: number | ||
15 | |||
16 | subtitleCheckbox: boolean | ||
17 | subtitle: string | ||
18 | |||
19 | loop: boolean | ||
20 | autoplay: boolean | ||
21 | muted: boolean | ||
22 | title: boolean | ||
23 | warningTitle: boolean | ||
24 | controls: boolean | ||
25 | } | ||
8 | 26 | ||
9 | @Component({ | 27 | @Component({ |
10 | selector: 'my-video-share', | 28 | selector: 'my-video-share', |
@@ -15,9 +33,13 @@ export class VideoShareComponent { | |||
15 | @ViewChild('modal') modal: ElementRef | 33 | @ViewChild('modal') modal: ElementRef |
16 | 34 | ||
17 | @Input() video: VideoDetails = null | 35 | @Input() video: VideoDetails = null |
36 | @Input() videoCaptions: VideoCaption[] = [] | ||
18 | 37 | ||
19 | currentVideoTimestamp: number | 38 | activeId: 'url' | 'qrcode' | 'embed' |
20 | startAtCheckbox = false | 39 | customizations: Customizations |
40 | isAdvancedCustomizationCollapsed = true | ||
41 | |||
42 | private currentVideoTimestamp: number | ||
21 | 43 | ||
22 | constructor ( | 44 | constructor ( |
23 | private modalService: NgbModal, | 45 | private modalService: NgbModal, |
@@ -26,19 +48,47 @@ export class VideoShareComponent { | |||
26 | ) { } | 48 | ) { } |
27 | 49 | ||
28 | show (currentVideoTimestamp?: number) { | 50 | show (currentVideoTimestamp?: number) { |
29 | this.currentVideoTimestamp = currentVideoTimestamp ? Math.floor(currentVideoTimestamp) : 0 | 51 | this.currentVideoTimestamp = currentVideoTimestamp |
52 | |||
53 | let subtitle: string | ||
54 | if (this.videoCaptions.length !== 0) { | ||
55 | subtitle = this.videoCaptions[0].language.id | ||
56 | } | ||
57 | |||
58 | this.customizations = { | ||
59 | startAtCheckbox: false, | ||
60 | startAt: currentVideoTimestamp ? Math.floor(currentVideoTimestamp) : 0, | ||
61 | |||
62 | stopAtCheckbox: false, | ||
63 | stopAt: this.video.duration, | ||
64 | |||
65 | subtitleCheckbox: false, | ||
66 | subtitle, | ||
67 | |||
68 | loop: false, | ||
69 | autoplay: false, | ||
70 | muted: false, | ||
71 | |||
72 | // Embed options | ||
73 | title: true, | ||
74 | warningTitle: true, | ||
75 | controls: true | ||
76 | } | ||
30 | 77 | ||
31 | this.modalService.open(this.modal) | 78 | this.modalService.open(this.modal) |
32 | } | 79 | } |
33 | 80 | ||
34 | getVideoIframeCode () { | 81 | getVideoIframeCode () { |
35 | const embedUrl = buildVideoLink(this.getVideoTimestampIfEnabled(), this.video.embedUrl) | 82 | const options = this.getOptions(this.video.embedUrl) |
36 | 83 | ||
84 | const embedUrl = buildVideoLink(options) | ||
37 | return buildVideoEmbed(embedUrl) | 85 | return buildVideoEmbed(embedUrl) |
38 | } | 86 | } |
39 | 87 | ||
40 | getVideoUrl () { | 88 | getVideoUrl () { |
41 | return buildVideoLink(this.getVideoTimestampIfEnabled()) | 89 | const options = this.getOptions() |
90 | |||
91 | return buildVideoLink(options) | ||
42 | } | 92 | } |
43 | 93 | ||
44 | notSecure () { | 94 | notSecure () { |
@@ -49,9 +99,30 @@ export class VideoShareComponent { | |||
49 | this.notifier.success(this.i18n('Copied')) | 99 | this.notifier.success(this.i18n('Copied')) |
50 | } | 100 | } |
51 | 101 | ||
52 | private getVideoTimestampIfEnabled () { | 102 | onTabChange (event: NgbTabChangeEvent) { |
53 | if (this.startAtCheckbox === true) return this.currentVideoTimestamp | 103 | this.activeId = event.nextId as any |
104 | } | ||
105 | |||
106 | isInEmbedTab () { | ||
107 | return this.activeId === 'embed' | ||
108 | } | ||
109 | |||
110 | private getOptions (baseUrl?: string) { | ||
111 | return { | ||
112 | baseUrl, | ||
113 | |||
114 | startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined, | ||
115 | stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined, | ||
116 | |||
117 | subtitle: this.customizations.subtitleCheckbox ? this.customizations.subtitle : undefined, | ||
118 | |||
119 | loop: this.customizations.loop, | ||
120 | autoplay: this.customizations.autoplay, | ||
121 | muted: this.customizations.muted, | ||
54 | 122 | ||
55 | return undefined | 123 | title: this.customizations.title, |
124 | warningTitle: this.customizations.warningTitle, | ||
125 | controls: this.customizations.controls | ||
126 | } | ||
56 | } | 127 | } |
57 | } | 128 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index 2e39b9c6b..6a02f630a 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -219,5 +219,5 @@ | |||
219 | 219 | ||
220 | <ng-template [ngIf]="video !== null"> | 220 | <ng-template [ngIf]="video !== null"> |
221 | <my-video-support #videoSupportModal [video]="video"></my-video-support> | 221 | <my-video-support #videoSupportModal [video]="video"></my-video-support> |
222 | <my-video-share #videoShareModal [video]="video"></my-video-share> | 222 | <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions"></my-video-share> |
223 | </ng-template> | 223 | </ng-template> |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index bada9bae8..35ea0fffd 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -347,6 +347,7 @@ $player-factor: 1.7; // 16/9 | |||
347 | /deep/ .other-videos { | 347 | /deep/ .other-videos { |
348 | padding-left: 15px; | 348 | padding-left: 15px; |
349 | flex-basis: $other-videos-width; | 349 | flex-basis: $other-videos-width; |
350 | min-width: $other-videos-width; | ||
350 | 351 | ||
351 | .title-page { | 352 | .title-page { |
352 | margin-top: 0 !important; | 353 | margin-top: 0 !important; |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 631504eab..3f1a98f89 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -6,7 +6,7 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | |||
6 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' | 6 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' |
7 | import { MetaService } from '@ngx-meta/core' | 7 | import { MetaService } from '@ngx-meta/core' |
8 | import { Notifier, ServerService } from '@app/core' | 8 | import { Notifier, ServerService } from '@app/core' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Observable, Subscription } from 'rxjs' |
10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' | 11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' |
12 | import { AuthService, ConfirmService } from '../../core' | 12 | import { AuthService, ConfirmService } from '../../core' |
@@ -20,6 +20,7 @@ import { environment } from '../../../environments/environment' | |||
20 | import { VideoCaptionService } from '@app/shared/video-caption' | 20 | import { VideoCaptionService } from '@app/shared/video-caption' |
21 | import { MarkdownService } from '@app/shared/renderer' | 21 | import { MarkdownService } from '@app/shared/renderer' |
22 | import { | 22 | import { |
23 | CustomizationOptions, | ||
23 | P2PMediaLoaderOptions, | 24 | P2PMediaLoaderOptions, |
24 | PeertubePlayerManager, | 25 | PeertubePlayerManager, |
25 | PeertubePlayerManagerOptions, | 26 | PeertubePlayerManagerOptions, |
@@ -28,8 +29,9 @@ import { | |||
28 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 29 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
29 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 30 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
30 | import { Video } from '@app/shared/video/video.model' | 31 | import { Video } from '@app/shared/video/video.model' |
31 | import { isWebRTCDisabled } from '../../../assets/player/utils' | 32 | import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' |
32 | import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' | 33 | import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' |
34 | import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage' | ||
33 | 35 | ||
34 | @Component({ | 36 | @Component({ |
35 | selector: 'my-video-watch', | 37 | selector: 'my-video-watch', |
@@ -48,9 +50,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
48 | playerElement: HTMLVideoElement | 50 | playerElement: HTMLVideoElement |
49 | theaterEnabled = false | 51 | theaterEnabled = false |
50 | userRating: UserVideoRateType = null | 52 | userRating: UserVideoRateType = null |
51 | video: VideoDetails = null | ||
52 | descriptionLoading = false | 53 | descriptionLoading = false |
53 | 54 | ||
55 | video: VideoDetails = null | ||
56 | videoCaptions: VideoCaption[] = [] | ||
57 | |||
54 | playlist: VideoPlaylist = null | 58 | playlist: VideoPlaylist = null |
55 | 59 | ||
56 | completeDescriptionShown = false | 60 | completeDescriptionShown = false |
@@ -120,6 +124,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
120 | }) | 124 | }) |
121 | 125 | ||
122 | this.initHotkeys() | 126 | this.initHotkeys() |
127 | |||
128 | this.theaterEnabled = getStoredTheater() | ||
123 | } | 129 | } |
124 | 130 | ||
125 | ngOnDestroy () { | 131 | ngOnDestroy () { |
@@ -135,22 +141,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
135 | 141 | ||
136 | setLike () { | 142 | setLike () { |
137 | if (this.isUserLoggedIn() === false) return | 143 | if (this.isUserLoggedIn() === false) return |
138 | if (this.userRating === 'like') { | 144 | |
139 | // Already liked this video | 145 | // Already liked this video |
140 | this.setRating('none') | 146 | if (this.userRating === 'like') this.setRating('none') |
141 | } else { | 147 | else this.setRating('like') |
142 | this.setRating('like') | ||
143 | } | ||
144 | } | 148 | } |
145 | 149 | ||
146 | setDislike () { | 150 | setDislike () { |
147 | if (this.isUserLoggedIn() === false) return | 151 | if (this.isUserLoggedIn() === false) return |
148 | if (this.userRating === 'dislike') { | 152 | |
149 | // Already disliked this video | 153 | // Already disliked this video |
150 | this.setRating('none') | 154 | if (this.userRating === 'dislike') this.setRating('none') |
151 | } else { | 155 | else this.setRating('dislike') |
152 | this.setRating('dislike') | ||
153 | } | ||
154 | } | 156 | } |
155 | 157 | ||
156 | showMoreDescription () { | 158 | showMoreDescription () { |
@@ -249,12 +251,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
249 | ) | 251 | ) |
250 | .subscribe(([ video, captionsResult ]) => { | 252 | .subscribe(([ video, captionsResult ]) => { |
251 | const queryParams = this.route.snapshot.queryParams | 253 | const queryParams = this.route.snapshot.queryParams |
252 | const startTime = queryParams.start | ||
253 | const stopTime = queryParams.stop | ||
254 | const subtitle = queryParams.subtitle | ||
255 | const playerMode = queryParams.mode | ||
256 | 254 | ||
257 | this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode }) | 255 | const urlOptions = { |
256 | startTime: queryParams.start, | ||
257 | stopTime: queryParams.stop, | ||
258 | |||
259 | muted: queryParams.muted, | ||
260 | loop: queryParams.loop, | ||
261 | subtitle: queryParams.subtitle, | ||
262 | |||
263 | playerMode: queryParams.mode, | ||
264 | peertubeLink: false | ||
265 | } | ||
266 | |||
267 | this.onVideoFetched(video, captionsResult.data, urlOptions) | ||
258 | .catch(err => this.handleError(err)) | 268 | .catch(err => this.handleError(err)) |
259 | }) | 269 | }) |
260 | } | 270 | } |
@@ -279,6 +289,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
279 | private updateVideoDescription (description: string) { | 289 | private updateVideoDescription (description: string) { |
280 | this.video.description = description | 290 | this.video.description = description |
281 | this.setVideoDescriptionHTML() | 291 | this.setVideoDescriptionHTML() |
292 | .catch(err => console.error(err)) | ||
282 | } | 293 | } |
283 | 294 | ||
284 | private async setVideoDescriptionHTML () { | 295 | private async setVideoDescriptionHTML () { |
@@ -327,9 +338,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
327 | private async onVideoFetched ( | 338 | private async onVideoFetched ( |
328 | video: VideoDetails, | 339 | video: VideoDetails, |
329 | videoCaptions: VideoCaption[], | 340 | videoCaptions: VideoCaption[], |
330 | urlOptions: { startTime?: number, stopTime?: number, subtitle?: string, playerMode?: string } | 341 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } |
331 | ) { | 342 | ) { |
332 | this.video = video | 343 | this.video = video |
344 | this.videoCaptions = videoCaptions | ||
333 | 345 | ||
334 | // Re init attributes | 346 | // Re init attributes |
335 | this.descriptionLoading = false | 347 | this.descriptionLoading = false |
@@ -339,7 +351,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
339 | 351 | ||
340 | this.videoWatchPlaylist.updatePlaylistIndex(video) | 352 | this.videoWatchPlaylist.updatePlaylistIndex(video) |
341 | 353 | ||
342 | let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) | 354 | let startTime = timeToInt(urlOptions.startTime) || (this.video.userHistory ? this.video.userHistory.currentTime : 0) |
343 | // If we are at the end of the video, reset the timer | 355 | // If we are at the end of the video, reset the timer |
344 | if (this.video.duration - startTime <= 1) startTime = 0 | 356 | if (this.video.duration - startTime <= 1) startTime = 0 |
345 | 357 | ||
@@ -378,20 +390,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
378 | enableHotkeys: true, | 390 | enableHotkeys: true, |
379 | inactivityTimeout: 2500, | 391 | inactivityTimeout: 2500, |
380 | poster: this.video.previewUrl, | 392 | poster: this.video.previewUrl, |
393 | |||
381 | startTime, | 394 | startTime, |
382 | stopTime: urlOptions.stopTime, | 395 | stopTime: urlOptions.stopTime, |
396 | controls: urlOptions.controls, | ||
397 | muted: urlOptions.muted, | ||
398 | loop: urlOptions.loop, | ||
399 | subtitle: urlOptions.subtitle, | ||
400 | |||
401 | peertubeLink: urlOptions.peertubeLink, | ||
383 | 402 | ||
384 | theaterMode: true, | 403 | theaterMode: true, |
385 | captions: videoCaptions.length !== 0, | 404 | captions: videoCaptions.length !== 0, |
386 | peertubeLink: false, | ||
387 | 405 | ||
388 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, | 406 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE |
407 | ? this.videoService.getVideoViewUrl(this.video.uuid) | ||
408 | : null, | ||
389 | embedUrl: this.video.embedUrl, | 409 | embedUrl: this.video.embedUrl, |
390 | 410 | ||
391 | language: this.localeId, | 411 | language: this.localeId, |
392 | 412 | ||
393 | subtitle: urlOptions.subtitle, | ||
394 | |||
395 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | 413 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { |
396 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | 414 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), |
397 | authorizationHeader: this.authService.getRequestHeaderValue() | 415 | authorizationHeader: this.authService.getRequestHeaderValue() |
@@ -433,7 +451,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
433 | 451 | ||
434 | this.zone.runOutsideAngular(async () => { | 452 | this.zone.runOutsideAngular(async () => { |
435 | this.player = await PeertubePlayerManager.initialize(mode, options) | 453 | this.player = await PeertubePlayerManager.initialize(mode, options) |
436 | this.theaterEnabled = this.player.theaterEnabled | ||
437 | 454 | ||
438 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) | 455 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) |
439 | 456 | ||
@@ -466,20 +483,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
466 | } | 483 | } |
467 | 484 | ||
468 | private setRating (nextRating: UserVideoRateType) { | 485 | private setRating (nextRating: UserVideoRateType) { |
469 | let method | 486 | const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = { |
470 | switch (nextRating) { | 487 | like: this.videoService.setVideoLike, |
471 | case 'like': | 488 | dislike: this.videoService.setVideoDislike, |
472 | method = this.videoService.setVideoLike | 489 | none: this.videoService.unsetVideoLike |
473 | break | ||
474 | case 'dislike': | ||
475 | method = this.videoService.setVideoDislike | ||
476 | break | ||
477 | case 'none': | ||
478 | method = this.videoService.unsetVideoLike | ||
479 | break | ||
480 | } | 490 | } |
481 | 491 | ||
482 | method.call(this.videoService, this.video.id) | 492 | ratingMethods[nextRating].call(this.videoService, this.video.id) |
483 | .subscribe( | 493 | .subscribe( |
484 | () => { | 494 | () => { |
485 | // Update the video like attribute | 495 | // Update the video like attribute |
@@ -545,25 +555,29 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
545 | private flushPlayer () { | 555 | private flushPlayer () { |
546 | // Remove player if it exists | 556 | // Remove player if it exists |
547 | if (this.player) { | 557 | if (this.player) { |
548 | this.player.dispose() | 558 | try { |
549 | this.player = undefined | 559 | this.player.dispose() |
560 | this.player = undefined | ||
561 | } catch (err) { | ||
562 | console.error('Cannot dispose player.', err) | ||
563 | } | ||
550 | } | 564 | } |
551 | } | 565 | } |
552 | 566 | ||
553 | private initHotkeys () { | 567 | private initHotkeys () { |
554 | this.hotkeys = [ | 568 | this.hotkeys = [ |
555 | new Hotkey('shift+l', (event: KeyboardEvent): boolean => { | 569 | new Hotkey('shift+l', () => { |
556 | this.setLike() | 570 | this.setLike() |
557 | return false | 571 | return false |
558 | }, undefined, this.i18n('Like the video')), | 572 | }, undefined, this.i18n('Like the video')), |
559 | new Hotkey('shift+d', (event: KeyboardEvent): boolean => { | 573 | |
574 | new Hotkey('shift+d', () => { | ||
560 | this.setDislike() | 575 | this.setDislike() |
561 | return false | 576 | return false |
562 | }, undefined, this.i18n('Dislike the video')), | 577 | }, undefined, this.i18n('Dislike the video')), |
563 | new Hotkey('shift+s', (event: KeyboardEvent): boolean => { | 578 | |
564 | this.subscribeButton.subscribed ? | 579 | new Hotkey('shift+s', () => { |
565 | this.subscribeButton.unsubscribe() : | 580 | this.subscribeButton.subscribed ? this.subscribeButton.unsubscribe() : this.subscribeButton.subscribe() |
566 | this.subscribeButton.subscribe() | ||
567 | return false | 581 | return false |
568 | }, undefined, this.i18n('Subscribe to the account')) | 582 | }, undefined, this.i18n('Subscribe to the account')) |
569 | ] | 583 | ] |
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts index 13d4023c2..65543343c 100644 --- a/client/src/app/videos/video-list/video-local.component.ts +++ b/client/src/app/videos/video-list/video-local.component.ts | |||
@@ -22,13 +22,13 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On | |||
22 | filter: VideoFilter = 'local' | 22 | filter: VideoFilter = 'local' |
23 | 23 | ||
24 | constructor ( | 24 | constructor ( |
25 | protected i18n: I18n, | ||
25 | protected router: Router, | 26 | protected router: Router, |
26 | protected serverService: ServerService, | 27 | protected serverService: ServerService, |
27 | protected route: ActivatedRoute, | 28 | protected route: ActivatedRoute, |
28 | protected notifier: Notifier, | 29 | protected notifier: Notifier, |
29 | protected authService: AuthService, | 30 | protected authService: AuthService, |
30 | protected screenService: ScreenService, | 31 | protected screenService: ScreenService, |
31 | private i18n: I18n, | ||
32 | private videoService: VideoService | 32 | private videoService: VideoService |
33 | ) { | 33 | ) { |
34 | super() | 34 | super() |
diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html index b644dd798..f59de584a 100644 --- a/client/src/app/videos/video-list/video-overview.component.html +++ b/client/src/app/videos/video-list/video-overview.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | <div class="no-results" i18n *ngIf="notResults">No results.</div> | 3 | <div class="no-results" i18n *ngIf="notResults">No results.</div> |
4 | 4 | ||
5 | <div class="section" *ngFor="let object of overview.categories"> | 5 | <div class="section" *ngFor="let object of overview.categories"> |
6 | <div class="section-title" i18n> | 6 | <div class="section-title"> |
7 | <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> | 7 | <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> |
8 | </div> | 8 | </div> |
9 | 9 | ||
@@ -11,7 +11,7 @@ | |||
11 | </div> | 11 | </div> |
12 | 12 | ||
13 | <div class="section" *ngFor="let object of overview.tags"> | 13 | <div class="section" *ngFor="let object of overview.tags"> |
14 | <div class="section-title" i18n> | 14 | <div class="section-title"> |
15 | <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> | 15 | <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> |
16 | </div> | 16 | </div> |
17 | 17 | ||
@@ -19,7 +19,7 @@ | |||
19 | </div> | 19 | </div> |
20 | 20 | ||
21 | <div class="section channel" *ngFor="let object of overview.channels"> | 21 | <div class="section channel" *ngFor="let object of overview.channels"> |
22 | <div class="section-title" i18n> | 22 | <div class="section-title"> |
23 | <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> | 23 | <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> |
24 | <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> | 24 | <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> |
25 | 25 | ||
diff --git a/client/src/app/videos/video-list/video-overview.component.scss b/client/src/app/videos/video-list/video-overview.component.scss index a24766783..ade6f53b7 100644 --- a/client/src/app/videos/video-list/video-overview.component.scss +++ b/client/src/app/videos/video-list/video-overview.component.scss | |||
@@ -2,62 +2,10 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .section { | 5 | .margin-content { |
6 | max-height: 500px; // 2 rows max | 6 | @include adapt-margin-content-width; |
7 | overflow: hidden; | ||
8 | padding-top: 10px; | ||
9 | |||
10 | &:first-child { | ||
11 | padding-top: 30px; | ||
12 | } | ||
13 | |||
14 | my-video-miniature { | ||
15 | text-align: left; | ||
16 | } | ||
17 | } | ||
18 | |||
19 | .section-title { | ||
20 | font-size: 24px; | ||
21 | font-weight: $font-semibold; | ||
22 | margin-bottom: 10px; | ||
23 | |||
24 | a { | ||
25 | &:hover, &:focus:not(.focus-visible), &:active { | ||
26 | text-decoration: none; | ||
27 | outline: none; | ||
28 | } | ||
29 | |||
30 | color: var(--mainForegroundColor); | ||
31 | } | ||
32 | } | 7 | } |
33 | 8 | ||
34 | .channel { | 9 | .section { |
35 | .section-title a { | 10 | @include miniature-rows; |
36 | display: flex; | ||
37 | width: fit-content; | ||
38 | align-items: center; | ||
39 | |||
40 | img { | ||
41 | @include avatar(28px); | ||
42 | |||
43 | margin-right: 8px; | ||
44 | } | ||
45 | } | ||
46 | } | ||
47 | |||
48 | @media screen and (max-width: 500px) { | ||
49 | .margin-content { | ||
50 | margin: 0 !important; | ||
51 | } | ||
52 | |||
53 | .section-title { | ||
54 | font-size: 17px; | ||
55 | } | ||
56 | |||
57 | .section { | ||
58 | max-height: initial; | ||
59 | overflow: initial; | ||
60 | |||
61 | @include video-miniature-small-screen; | ||
62 | } | ||
63 | } | 11 | } |
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts index 80cef813e..f54bade98 100644 --- a/client/src/app/videos/video-list/video-recently-added.component.ts +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -17,15 +17,16 @@ import { Notifier, ServerService } from '@app/core' | |||
17 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { | 17 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { |
18 | titlePage: string | 18 | titlePage: string |
19 | sort: VideoSortField = '-publishedAt' | 19 | sort: VideoSortField = '-publishedAt' |
20 | groupByDate = true | ||
20 | 21 | ||
21 | constructor ( | 22 | constructor ( |
23 | protected i18n: I18n, | ||
22 | protected route: ActivatedRoute, | 24 | protected route: ActivatedRoute, |
23 | protected serverService: ServerService, | 25 | protected serverService: ServerService, |
24 | protected router: Router, | 26 | protected router: Router, |
25 | protected notifier: Notifier, | 27 | protected notifier: Notifier, |
26 | protected authService: AuthService, | 28 | protected authService: AuthService, |
27 | protected screenService: ScreenService, | 29 | protected screenService: ScreenService, |
28 | private i18n: I18n, | ||
29 | private videoService: VideoService | 30 | private videoService: VideoService |
30 | ) { | 31 | ) { |
31 | super() | 32 | super() |
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index e2ad95bc4..a2c819ebe 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -19,13 +19,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
19 | defaultSort: VideoSortField = '-trending' | 19 | defaultSort: VideoSortField = '-trending' |
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | protected i18n: I18n, | ||
22 | protected router: Router, | 23 | protected router: Router, |
23 | protected serverService: ServerService, | 24 | protected serverService: ServerService, |
24 | protected route: ActivatedRoute, | 25 | protected route: ActivatedRoute, |
25 | protected notifier: Notifier, | 26 | protected notifier: Notifier, |
26 | protected authService: AuthService, | 27 | protected authService: AuthService, |
27 | protected screenService: ScreenService, | 28 | protected screenService: ScreenService, |
28 | private i18n: I18n, | ||
29 | private videoService: VideoService | 29 | private videoService: VideoService |
30 | ) { | 30 | ) { |
31 | super() | 31 | super() |
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 2f0685ccc..3caa371d8 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 | |||
@@ -19,15 +19,16 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement | |||
19 | titlePage: string | 19 | titlePage: string |
20 | sort = '-publishedAt' as VideoSortField | 20 | sort = '-publishedAt' as VideoSortField |
21 | ownerDisplayType: OwnerDisplayType = 'auto' | 21 | ownerDisplayType: OwnerDisplayType = 'auto' |
22 | groupByDate = true | ||
22 | 23 | ||
23 | constructor ( | 24 | constructor ( |
25 | protected i18n: I18n, | ||
24 | protected router: Router, | 26 | protected router: Router, |
25 | protected serverService: ServerService, | 27 | protected serverService: ServerService, |
26 | protected route: ActivatedRoute, | 28 | protected route: ActivatedRoute, |
27 | protected notifier: Notifier, | 29 | protected notifier: Notifier, |
28 | protected authService: AuthService, | 30 | protected authService: AuthService, |
29 | protected screenService: ScreenService, | 31 | protected screenService: ScreenService, |
30 | private i18n: I18n, | ||
31 | private videoService: VideoService | 32 | private videoService: VideoService |
32 | ) { | 33 | ) { |
33 | super() | 34 | super() |
diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts index 059fca308..f6c5c5419 100644 --- a/client/src/assets/player/peertube-player-local-storage.ts +++ b/client/src/assets/player/peertube-player-local-storage.ts | |||
@@ -29,7 +29,7 @@ function getStoredTheater () { | |||
29 | const value = getLocalStorage('theater-enabled') | 29 | const value = getLocalStorage('theater-enabled') |
30 | if (value !== null && value !== undefined) return value === 'true' | 30 | if (value !== null && value !== undefined) return value === 'true' |
31 | 31 | ||
32 | return undefined | 32 | return false |
33 | } | 33 | } |
34 | 34 | ||
35 | function saveVolumeInStore (value: number) { | 35 | function saveVolumeInStore (value: number) { |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 6cdd54372..083c621d2 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -39,7 +39,19 @@ export type P2PMediaLoaderOptions = { | |||
39 | videoFiles: VideoFile[] | 39 | videoFiles: VideoFile[] |
40 | } | 40 | } |
41 | 41 | ||
42 | export type CommonOptions = { | 42 | export interface CustomizationOptions { |
43 | startTime: number | string | ||
44 | stopTime: number | string | ||
45 | |||
46 | controls?: boolean | ||
47 | muted?: boolean | ||
48 | loop?: boolean | ||
49 | subtitle?: string | ||
50 | |||
51 | peertubeLink: boolean | ||
52 | } | ||
53 | |||
54 | export interface CommonOptions extends CustomizationOptions { | ||
43 | playerElement: HTMLVideoElement | 55 | playerElement: HTMLVideoElement |
44 | onPlayerElementChange: (element: HTMLVideoElement) => void | 56 | onPlayerElementChange: (element: HTMLVideoElement) => void |
45 | 57 | ||
@@ -48,21 +60,14 @@ export type CommonOptions = { | |||
48 | enableHotkeys: boolean | 60 | enableHotkeys: boolean |
49 | inactivityTimeout: number | 61 | inactivityTimeout: number |
50 | poster: string | 62 | poster: string |
51 | startTime: number | string | ||
52 | stopTime: number | string | ||
53 | 63 | ||
54 | theaterMode: boolean | 64 | theaterMode: boolean |
55 | captions: boolean | 65 | captions: boolean |
56 | peertubeLink: boolean | ||
57 | 66 | ||
58 | videoViewUrl: string | 67 | videoViewUrl: string |
59 | embedUrl: string | 68 | embedUrl: string |
60 | 69 | ||
61 | language?: string | 70 | language?: string |
62 | controls?: boolean | ||
63 | muted?: boolean | ||
64 | loop?: boolean | ||
65 | subtitle?: string | ||
66 | 71 | ||
67 | videoCaptions: VideoJSCaption[] | 72 | videoCaptions: VideoJSCaption[] |
68 | 73 | ||
@@ -117,8 +122,17 @@ export class PeertubePlayerManager { | |||
117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | 122 | videojs(options.common.playerElement, videojsOptions, function (this: any) { |
118 | const player = this | 123 | const player = this |
119 | 124 | ||
120 | player.tech_.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) | 125 | let alreadyFallback = false |
121 | player.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) | 126 | |
127 | player.tech_.one('error', () => { | ||
128 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | ||
129 | alreadyFallback = true | ||
130 | }) | ||
131 | |||
132 | player.one('error', () => { | ||
133 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | ||
134 | alreadyFallback = true | ||
135 | }) | ||
122 | 136 | ||
123 | self.addContextMenu(mode, player, options.common.embedUrl) | 137 | self.addContextMenu(mode, player, options.common.embedUrl) |
124 | 138 | ||
@@ -432,7 +446,7 @@ export class PeertubePlayerManager { | |||
432 | label: player.localize('Copy the video URL at the current time'), | 446 | label: player.localize('Copy the video URL at the current time'), |
433 | listener: function () { | 447 | listener: function () { |
434 | const player = this as videojs.Player | 448 | const player = this as videojs.Player |
435 | copyToClipboard(buildVideoLink(player.currentTime())) | 449 | copyToClipboard(buildVideoLink({ startTime: player.currentTime() })) |
436 | } | 450 | } |
437 | }, | 451 | }, |
438 | { | 452 | { |
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 366689962..777abb568 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -27,18 +27,55 @@ function isMobile () { | |||
27 | return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) | 27 | return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) |
28 | } | 28 | } |
29 | 29 | ||
30 | function buildVideoLink (time?: number, url?: string) { | 30 | function buildVideoLink (options: { |
31 | if (!url) url = window.location.origin + window.location.pathname.replace('/embed/', '/watch/') | 31 | baseUrl?: string, |
32 | 32 | ||
33 | if (time) { | 33 | startTime?: number, |
34 | const timeInt = Math.floor(time) | 34 | stopTime?: number, |
35 | 35 | ||
36 | const params = new URLSearchParams(window.location.search) | 36 | subtitle?: string, |
37 | params.set('start', secondsToTime(timeInt)) | ||
38 | 37 | ||
39 | return url + '?' + params.toString() | 38 | loop?: boolean, |
39 | autoplay?: boolean, | ||
40 | muted?: boolean, | ||
41 | |||
42 | // Embed options | ||
43 | title?: boolean, | ||
44 | warningTitle?: boolean, | ||
45 | controls?: boolean | ||
46 | } = {}) { | ||
47 | const { baseUrl } = options | ||
48 | |||
49 | const url = baseUrl | ||
50 | ? baseUrl | ||
51 | : window.location.origin + window.location.pathname.replace('/embed/', '/watch/') | ||
52 | |||
53 | const params = new URLSearchParams(window.location.search) | ||
54 | |||
55 | if (options.startTime) { | ||
56 | const startTimeInt = Math.floor(options.startTime) | ||
57 | params.set('start', secondsToTime(startTimeInt)) | ||
58 | } | ||
59 | |||
60 | if (options.stopTime) { | ||
61 | const stopTimeInt = Math.floor(options.stopTime) | ||
62 | params.set('stop', secondsToTime(stopTimeInt)) | ||
40 | } | 63 | } |
41 | 64 | ||
65 | if (options.subtitle) params.set('subtitle', options.subtitle) | ||
66 | |||
67 | if (options.loop === true) params.set('loop', '1') | ||
68 | if (options.autoplay === true) params.set('autoplay', '1') | ||
69 | if (options.muted === true) params.set('muted', '1') | ||
70 | if (options.title === false) params.set('title', '0') | ||
71 | if (options.warningTitle === false) params.set('warningTitle', '0') | ||
72 | if (options.controls === false) params.set('controls', '0') | ||
73 | |||
74 | let hasParams = false | ||
75 | params.forEach(() => hasParams = true) | ||
76 | |||
77 | if (hasParams) return url + '?' + params.toString() | ||
78 | |||
42 | return url | 79 | return url |
43 | } | 80 | } |
44 | 81 | ||
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index fed8ea33e..4d0ea37f5 100644 --- a/client/src/assets/player/videojs-components/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -16,7 +16,7 @@ class PeerTubeLinkButton extends Button { | |||
16 | } | 16 | } |
17 | 17 | ||
18 | updateHref () { | 18 | updateHref () { |
19 | this.el().setAttribute('href', buildVideoLink(this.player().currentTime())) | 19 | this.el().setAttribute('href', buildVideoLink({ startTime: this.player().currentTime() })) |
20 | } | 20 | } |
21 | 21 | ||
22 | handleClick () { | 22 | handleClick () { |
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index d84766240..c64a8ebf8 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -12,6 +12,7 @@ $assets-path: '../assets/'; | |||
12 | @import './player/index'; | 12 | @import './player/index'; |
13 | @import './loading-bar'; | 13 | @import './loading-bar'; |
14 | 14 | ||
15 | @import './bootstrap'; | ||
15 | @import './primeng-custom'; | 16 | @import './primeng-custom'; |
16 | 17 | ||
17 | [hidden] { | 18 | [hidden] { |
@@ -181,128 +182,11 @@ label { | |||
181 | font-weight: bold; | 182 | font-weight: bold; |
182 | } | 183 | } |
183 | 184 | ||
184 | // Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d | ||
185 | .glyphicon-refresh-animate { | ||
186 | animation: spin .7s infinite linear; | ||
187 | } | ||
188 | |||
189 | @keyframes spin { | 185 | @keyframes spin { |
190 | from { transform: scale(1) rotate(0deg);} | 186 | from { transform: scale(1) rotate(0deg);} |
191 | to { transform: scale(1) rotate(360deg);} | 187 | to { transform: scale(1) rotate(360deg);} |
192 | } | 188 | } |
193 | 189 | ||
194 | // Bootstrap customizations | ||
195 | .dropdown-menu { | ||
196 | border-radius: 3px; | ||
197 | box-shadow: 0 3px 6px; | ||
198 | font-size: 15px; | ||
199 | |||
200 | .dropdown-item { | ||
201 | padding: 3px 15px; | ||
202 | |||
203 | &:active { | ||
204 | color: #000 !important; | ||
205 | } | ||
206 | } | ||
207 | |||
208 | button { | ||
209 | @include disable-default-a-behaviour; | ||
210 | } | ||
211 | |||
212 | a { | ||
213 | @include disable-default-a-behaviour; | ||
214 | color: #000 !important; | ||
215 | } | ||
216 | } | ||
217 | |||
218 | .modal { | ||
219 | .modal-content { | ||
220 | background-color: var(--mainBackgroundColor); | ||
221 | } | ||
222 | |||
223 | .modal-header { | ||
224 | border-bottom: none; | ||
225 | margin-bottom: 5px; | ||
226 | |||
227 | .modal-title { | ||
228 | font-size: 20px; | ||
229 | font-weight: $font-semibold; | ||
230 | } | ||
231 | |||
232 | my-global-icon { | ||
233 | @include icon(24px); | ||
234 | |||
235 | position: relative; | ||
236 | top: 3px; | ||
237 | float: right; | ||
238 | |||
239 | margin: 0; | ||
240 | padding: 0; | ||
241 | opacity: 1; | ||
242 | } | ||
243 | } | ||
244 | |||
245 | .inputs { | ||
246 | margin-bottom: 0; | ||
247 | text-align: right; | ||
248 | |||
249 | .action-button-cancel { | ||
250 | @include peertube-button; | ||
251 | @include grey-button; | ||
252 | |||
253 | display: inline-block; | ||
254 | margin-right: 10px; | ||
255 | } | ||
256 | |||
257 | .action-button-submit { | ||
258 | @include peertube-button; | ||
259 | @include orange-button; | ||
260 | } | ||
261 | } | ||
262 | } | ||
263 | |||
264 | // Nav customizations | ||
265 | .nav .nav-link { | ||
266 | display: flex !important; | ||
267 | align-items: center; | ||
268 | height: 30px !important; | ||
269 | padding: 10px 15px !important; | ||
270 | } | ||
271 | |||
272 | .nav.nav-pills { | ||
273 | font-size: 16px !important; | ||
274 | |||
275 | .nav-link.active { | ||
276 | font-weight: $font-semibold !important; | ||
277 | } | ||
278 | |||
279 | a { | ||
280 | @include disable-default-a-behaviour; | ||
281 | |||
282 | color: var(--mainForegroundColor); | ||
283 | } | ||
284 | } | ||
285 | |||
286 | ngb-tabset.bootstrap { | ||
287 | |||
288 | .nav-link { | ||
289 | &, & a { | ||
290 | @include disable-default-a-behaviour; | ||
291 | |||
292 | color: var(--mainForegroundColor) !important; | ||
293 | } | ||
294 | } | ||
295 | |||
296 | .nav-pills .nav-link.active { | ||
297 | color: #000 !important; | ||
298 | } | ||
299 | } | ||
300 | |||
301 | .nav-tabs .nav-link.active { | ||
302 | background-color: var(--mainBackgroundColor) !important; | ||
303 | border-bottom: none; | ||
304 | } | ||
305 | |||
306 | .orange-button { | 190 | .orange-button { |
307 | @include peertube-button; | 191 | @include peertube-button; |
308 | @include orange-button; | 192 | @include orange-button; |
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss new file mode 100644 index 000000000..12e73278a --- /dev/null +++ b/client/src/sass/bootstrap.scss | |||
@@ -0,0 +1,138 @@ | |||
1 | $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; | ||
2 | @import '_bootstrap'; | ||
3 | |||
4 | @import '_variables'; | ||
5 | @import '_mixins'; | ||
6 | |||
7 | // Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d | ||
8 | .glyphicon-refresh-animate { | ||
9 | animation: spin .7s infinite linear; | ||
10 | } | ||
11 | |||
12 | @keyframes spin { | ||
13 | from { transform: scale(1) rotate(0deg);} | ||
14 | to { transform: scale(1) rotate(360deg);} | ||
15 | } | ||
16 | |||
17 | .dropdown-menu { | ||
18 | border-radius: 3px; | ||
19 | box-shadow: 0 3px 6px; | ||
20 | font-size: 15px; | ||
21 | |||
22 | .dropdown-item { | ||
23 | padding: 3px 15px; | ||
24 | |||
25 | &:active { | ||
26 | color: #000 !important; | ||
27 | } | ||
28 | } | ||
29 | |||
30 | button { | ||
31 | @include disable-default-a-behaviour; | ||
32 | } | ||
33 | |||
34 | a { | ||
35 | @include disable-default-a-behaviour; | ||
36 | color: #000 !important; | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .modal { | ||
41 | .modal-content { | ||
42 | background-color: var(--mainBackgroundColor); | ||
43 | } | ||
44 | |||
45 | .modal-header { | ||
46 | border-bottom: none; | ||
47 | margin-bottom: 5px; | ||
48 | |||
49 | .modal-title { | ||
50 | font-size: 20px; | ||
51 | font-weight: $font-semibold; | ||
52 | } | ||
53 | |||
54 | my-global-icon { | ||
55 | @include icon(24px); | ||
56 | |||
57 | position: relative; | ||
58 | top: 3px; | ||
59 | float: right; | ||
60 | |||
61 | margin: 0; | ||
62 | padding: 0; | ||
63 | opacity: 1; | ||
64 | } | ||
65 | } | ||
66 | |||
67 | .inputs { | ||
68 | margin-bottom: 0; | ||
69 | text-align: right; | ||
70 | |||
71 | .action-button-cancel { | ||
72 | @include peertube-button; | ||
73 | @include grey-button; | ||
74 | |||
75 | display: inline-block; | ||
76 | margin-right: 10px; | ||
77 | } | ||
78 | |||
79 | .action-button-submit { | ||
80 | @include peertube-button; | ||
81 | @include orange-button; | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | |||
86 | // Nav customizations | ||
87 | .nav .nav-link { | ||
88 | display: flex !important; | ||
89 | align-items: center; | ||
90 | height: 30px !important; | ||
91 | padding: 10px 15px !important; | ||
92 | } | ||
93 | |||
94 | .nav.nav-pills { | ||
95 | font-size: 16px !important; | ||
96 | |||
97 | .nav-link.active { | ||
98 | font-weight: $font-semibold !important; | ||
99 | } | ||
100 | |||
101 | a { | ||
102 | @include disable-default-a-behaviour; | ||
103 | |||
104 | color: var(--mainForegroundColor); | ||
105 | } | ||
106 | } | ||
107 | |||
108 | ngb-tabset.bootstrap { | ||
109 | |||
110 | .nav-link { | ||
111 | &, & a { | ||
112 | @include disable-default-a-behaviour; | ||
113 | |||
114 | color: var(--mainForegroundColor) !important; | ||
115 | } | ||
116 | } | ||
117 | |||
118 | .nav-pills .nav-link.active { | ||
119 | color: #000 !important; | ||
120 | } | ||
121 | } | ||
122 | |||
123 | .nav-tabs .nav-link.active { | ||
124 | background-color: var(--mainBackgroundColor) !important; | ||
125 | border-bottom: none; | ||
126 | } | ||
127 | |||
128 | .collapse-transition { | ||
129 | // Animation when we show/hide the filters | ||
130 | transition: max-height 0.3s; | ||
131 | display: block !important; | ||
132 | overflow: hidden !important; | ||
133 | max-height: 0; | ||
134 | |||
135 | &.show { | ||
136 | max-height: 1500px; | ||
137 | } | ||
138 | } | ||
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss index b62187fd2..0c2ee2d0d 100644 --- a/client/src/sass/include/_miniature.scss +++ b/client/src/sass/include/_miniature.scss | |||
@@ -138,3 +138,100 @@ $play-overlay-width: 18px; | |||
138 | } | 138 | } |
139 | } | 139 | } |
140 | } | 140 | } |
141 | |||
142 | @mixin miniature-rows { | ||
143 | max-height: 540px; // 2 rows max | ||
144 | overflow: hidden; | ||
145 | padding-top: 10px; | ||
146 | |||
147 | &:first-child { | ||
148 | padding-top: 30px; | ||
149 | } | ||
150 | |||
151 | my-video-miniature { | ||
152 | text-align: left; | ||
153 | } | ||
154 | |||
155 | .section-title { | ||
156 | font-size: 24px; | ||
157 | font-weight: $font-semibold; | ||
158 | margin-bottom: 30px; | ||
159 | display: flex; | ||
160 | justify-content: space-between; | ||
161 | |||
162 | a { | ||
163 | &:hover, &:focus:not(.focus-visible), &:active { | ||
164 | text-decoration: none; | ||
165 | outline: none; | ||
166 | } | ||
167 | |||
168 | color: var(--mainForegroundColor); | ||
169 | } | ||
170 | } | ||
171 | |||
172 | &.channel { | ||
173 | .section-title { | ||
174 | a { | ||
175 | display: flex; | ||
176 | width: fit-content; | ||
177 | align-items: center; | ||
178 | |||
179 | img { | ||
180 | @include avatar(28px); | ||
181 | |||
182 | margin-right: 8px; | ||
183 | } | ||
184 | } | ||
185 | |||
186 | .followers { | ||
187 | color: $grey-foreground-color; | ||
188 | font-weight: normal; | ||
189 | font-size: 14px; | ||
190 | margin-left: 10px; | ||
191 | position: relative; | ||
192 | top: 2px; | ||
193 | } | ||
194 | } | ||
195 | } | ||
196 | |||
197 | @media screen and (max-width: $mobile-view) { | ||
198 | max-height: initial; | ||
199 | overflow: initial; | ||
200 | |||
201 | @include video-miniature-small-screen; | ||
202 | |||
203 | .section-title { | ||
204 | font-size: 17px; | ||
205 | } | ||
206 | } | ||
207 | } | ||
208 | |||
209 | @mixin adapt-margin-content-width { | ||
210 | width: $video-miniature-width * 6; | ||
211 | margin: auto !important; | ||
212 | |||
213 | @media screen and (max-width: 1800px) { | ||
214 | width: $video-miniature-width * 5; | ||
215 | } | ||
216 | |||
217 | @media screen and (max-width: 1800px - $video-miniature-width) { | ||
218 | width: $video-miniature-width * 4; | ||
219 | } | ||
220 | |||
221 | @media screen and (max-width: 1800px - (2* $video-miniature-width)) { | ||
222 | width: $video-miniature-width * 3; | ||
223 | } | ||
224 | |||
225 | @media screen and (max-width: 1800px - (3* $video-miniature-width)) { | ||
226 | width: $video-miniature-width * 2; | ||
227 | } | ||
228 | |||
229 | @media screen and (max-width: 500px) { | ||
230 | width: auto; | ||
231 | margin: 0 !important; | ||
232 | |||
233 | .videos { | ||
234 | @include video-miniature-small-screen; | ||
235 | } | ||
236 | } | ||
237 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 262a8136f..f608e9299 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -235,6 +235,14 @@ | |||
235 | position: relative; | 235 | position: relative; |
236 | font-size: 15px; | 236 | font-size: 15px; |
237 | 237 | ||
238 | &.disabled { | ||
239 | background-color: #E5E5E5; | ||
240 | |||
241 | select { | ||
242 | cursor: default; | ||
243 | } | ||
244 | } | ||
245 | |||
238 | @media screen and (max-width: $width) { | 246 | @media screen and (max-width: $width) { |
239 | width: 100%; | 247 | width: 100%; |
240 | } | 248 | } |
@@ -282,16 +290,6 @@ | |||
282 | } | 290 | } |
283 | } | 291 | } |
284 | 292 | ||
285 | @mixin peertube-select-disabled-container ($width) { | ||
286 | @include peertube-select-container($width); | ||
287 | |||
288 | background-color: #E5E5E5; | ||
289 | |||
290 | select { | ||
291 | cursor: default; | ||
292 | } | ||
293 | } | ||
294 | |||
295 | // Thanks: https://codepen.io/triss90/pen/XNEdRe/ | 293 | // Thanks: https://codepen.io/triss90/pen/XNEdRe/ |
296 | @mixin peertube-radio-container { | 294 | @mixin peertube-radio-container { |
297 | input[type="radio"] { | 295 | input[type="radio"] { |
@@ -331,7 +329,12 @@ | |||
331 | } | 329 | } |
332 | 330 | ||
333 | @mixin peertube-checkbox ($border-width) { | 331 | @mixin peertube-checkbox ($border-width) { |
334 | display: none; | 332 | opacity: 0; |
333 | position: absolute; | ||
334 | |||
335 | &:focus + span { | ||
336 | outline: 1px solid #1e5180; | ||
337 | } | ||
335 | 338 | ||
336 | & + span { | 339 | & + span { |
337 | position: relative; | 340 | position: relative; |
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index c7b205b11..aafeda257 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -71,7 +71,9 @@ $variables: ( | |||
71 | --menuForegroundColor: var(--menuForegroundColor), | 71 | --menuForegroundColor: var(--menuForegroundColor), |
72 | --submenuColor: var(--submenuColor), | 72 | --submenuColor: var(--submenuColor), |
73 | --inputColor: var(--inputColor), | 73 | --inputColor: var(--inputColor), |
74 | --inputPlaceholderColor: var(--inputPlaceholderColor) | 74 | --inputPlaceholderColor: var(--inputPlaceholderColor), |
75 | --embedForegroundColor: var(--embedForegroundColor), | ||
76 | --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) | ||
75 | ); | 77 | ); |
76 | 78 | ||
77 | /*** theme helper ***/ | 79 | /*** theme helper ***/ |
diff --git a/client/src/sass/player/_player-variables.scss b/client/src/sass/player/_player-variables.scss index 110129790..4e9e8736c 100644 --- a/client/src/sass/player/_player-variables.scss +++ b/client/src/sass/player/_player-variables.scss | |||
@@ -10,4 +10,10 @@ $slider-bg-color: lighten($primary-background-color, 33%); | |||
10 | 10 | ||
11 | $progress-margin: 10px; | 11 | $progress-margin: 10px; |
12 | 12 | ||
13 | $assets-path: '../../assets/' !default; \ No newline at end of file | 13 | $assets-path: '../../assets/' !default; |
14 | |||
15 | body { | ||
16 | --embedForegroundColor: #{$primary-foreground-color}; | ||
17 | |||
18 | --embedBigPlayBackgroundColor: #{$primary-background-color}; | ||
19 | } | ||
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss index 71d6d1b1d..eeab0ccdf 100644 --- a/client/src/sass/player/context-menu.scss +++ b/client/src/sass/player/context-menu.scss | |||
@@ -14,7 +14,7 @@ $context-menu-width: 350px; | |||
14 | 14 | ||
15 | .vjs-menu-content { | 15 | .vjs-menu-content { |
16 | opacity: $primary-foreground-opacity; | 16 | opacity: $primary-foreground-opacity; |
17 | color: $primary-foreground-color; | 17 | color: var(--embedForegroundCsolor); |
18 | font-size: $font-size !important; | 18 | font-size: $font-size !important; |
19 | font-weight: $font-semibold; | 19 | font-weight: $font-semibold; |
20 | } | 20 | } |
@@ -30,4 +30,4 @@ $context-menu-width: 350px; | |||
30 | background-color: rgba(255, 255, 255, 0.2); | 30 | background-color: rgba(255, 255, 255, 0.2); |
31 | } | 31 | } |
32 | } | 32 | } |
33 | } \ No newline at end of file | 33 | } |
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index e63a2875c..996024ade 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss | |||
@@ -10,9 +10,8 @@ | |||
10 | } | 10 | } |
11 | 11 | ||
12 | .video-js.vjs-peertube-skin { | 12 | .video-js.vjs-peertube-skin { |
13 | |||
14 | font-size: $font-size; | 13 | font-size: $font-size; |
15 | color: $primary-foreground-color; | 14 | color: var(--embedForegroundColor); |
16 | 15 | ||
17 | .vjs-dock-text { | 16 | .vjs-dock-text { |
18 | padding-right: 10px; | 17 | padding-right: 10px; |
@@ -114,7 +113,7 @@ | |||
114 | .vjs-control-bar, | 113 | .vjs-control-bar, |
115 | .vjs-big-play-button, | 114 | .vjs-big-play-button, |
116 | .vjs-settings-dialog { | 115 | .vjs-settings-dialog { |
117 | background-color: rgba($primary-background-color, 0.5); | 116 | background-color: var(--embedBigPlayBackgroundColor); |
118 | } | 117 | } |
119 | 118 | ||
120 | .vjs-poster { | 119 | .vjs-poster { |
@@ -139,7 +138,8 @@ | |||
139 | .vjs-theater-control, | 138 | .vjs-theater-control, |
140 | .vjs-settings | 139 | .vjs-settings |
141 | { | 140 | { |
142 | color: $primary-foreground-color !important; | 141 | color: var(--embedForegroundColor) !important; |
142 | |||
143 | opacity: $primary-foreground-opacity; | 143 | opacity: $primary-foreground-opacity; |
144 | transition: opacity .1s; | 144 | transition: opacity .1s; |
145 | 145 | ||
@@ -151,7 +151,7 @@ | |||
151 | .vjs-current-time, | 151 | .vjs-current-time, |
152 | .vjs-duration, | 152 | .vjs-duration, |
153 | .vjs-peertube { | 153 | .vjs-peertube { |
154 | color: $primary-foreground-color; | 154 | color: var(--embedForegroundColor); |
155 | opacity: $primary-foreground-opacity; | 155 | opacity: $primary-foreground-opacity; |
156 | } | 156 | } |
157 | 157 | ||
@@ -171,7 +171,7 @@ | |||
171 | transition: none; | 171 | transition: none; |
172 | 172 | ||
173 | .vjs-play-progress { | 173 | .vjs-play-progress { |
174 | background: $primary-foreground-color; | 174 | background: var(--embedForegroundColor); |
175 | 175 | ||
176 | // Not display the circle if the progress is not hovered | 176 | // Not display the circle if the progress is not hovered |
177 | &::before { | 177 | &::before { |
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss index 61965c85e..83407b445 100644 --- a/client/src/sass/player/settings-menu.scss +++ b/client/src/sass/player/settings-menu.scss | |||
@@ -38,7 +38,7 @@ $setting-transition-easing: ease-out; | |||
38 | position: absolute; | 38 | position: absolute; |
39 | right: .5em; | 39 | right: .5em; |
40 | bottom: 3.5em; | 40 | bottom: 3.5em; |
41 | color: $primary-foreground-color; | 41 | color: var(--embedForegroundColor); |
42 | opacity: $primary-foreground-opacity; | 42 | opacity: $primary-foreground-opacity; |
43 | margin: 0 auto; | 43 | margin: 0 auto; |
44 | font-size: $font-size !important; | 44 | font-size: $font-size !important; |
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts new file mode 100644 index 000000000..169e371da --- /dev/null +++ b/client/src/standalone/videos/embed-api.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | import './embed.scss' | ||
2 | |||
3 | import * as Channel from 'jschannel' | ||
4 | import { PeerTubeResolution } from '../player/definitions' | ||
5 | import { PeerTubeEmbed } from './embed' | ||
6 | |||
7 | /** | ||
8 | * Embed API exposes control of the embed player to the outside world via | ||
9 | * JSChannels and window.postMessage | ||
10 | */ | ||
11 | export class PeerTubeEmbedApi { | ||
12 | private channel: Channel.MessagingChannel | ||
13 | private isReady = false | ||
14 | private resolutions: PeerTubeResolution[] = null | ||
15 | |||
16 | constructor (private embed: PeerTubeEmbed) { | ||
17 | } | ||
18 | |||
19 | initialize () { | ||
20 | this.constructChannel() | ||
21 | this.setupStateTracking() | ||
22 | |||
23 | // We're ready! | ||
24 | |||
25 | this.notifyReady() | ||
26 | } | ||
27 | |||
28 | private get element () { | ||
29 | return this.embed.videoElement | ||
30 | } | ||
31 | |||
32 | private constructChannel () { | ||
33 | const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) | ||
34 | |||
35 | channel.bind('play', (txn, params) => this.embed.player.play()) | ||
36 | channel.bind('pause', (txn, params) => this.embed.player.pause()) | ||
37 | channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) | ||
38 | channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) | ||
39 | channel.bind('getVolume', (txn, value) => this.embed.player.volume()) | ||
40 | channel.bind('isReady', (txn, params) => this.isReady) | ||
41 | channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) | ||
42 | channel.bind('getResolutions', (txn, params) => this.resolutions) | ||
43 | channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) | ||
44 | channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) | ||
45 | channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) | ||
46 | |||
47 | this.channel = channel | ||
48 | } | ||
49 | |||
50 | private setResolution (resolutionId: number) { | ||
51 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return | ||
52 | |||
53 | // Auto resolution | ||
54 | if (resolutionId === -1) { | ||
55 | this.embed.player.webtorrent().enableAutoResolution() | ||
56 | return | ||
57 | } | ||
58 | |||
59 | this.embed.player.webtorrent().disableAutoResolution() | ||
60 | this.embed.player.webtorrent().updateResolution(resolutionId) | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * Let the host know that we're ready to go! | ||
65 | */ | ||
66 | private notifyReady () { | ||
67 | this.isReady = true | ||
68 | this.channel.notify({ method: 'ready', params: true }) | ||
69 | } | ||
70 | |||
71 | private setupStateTracking () { | ||
72 | let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted' | ||
73 | |||
74 | setInterval(() => { | ||
75 | const position = this.element.currentTime | ||
76 | const volume = this.element.volume | ||
77 | |||
78 | this.channel.notify({ | ||
79 | method: 'playbackStatusUpdate', | ||
80 | params: { | ||
81 | position, | ||
82 | volume, | ||
83 | playbackState: currentState | ||
84 | } | ||
85 | }) | ||
86 | }, 500) | ||
87 | |||
88 | this.element.addEventListener('play', ev => { | ||
89 | currentState = 'playing' | ||
90 | this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) | ||
91 | }) | ||
92 | |||
93 | this.element.addEventListener('pause', ev => { | ||
94 | currentState = 'paused' | ||
95 | this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) | ||
96 | }) | ||
97 | |||
98 | // PeerTube specific capabilities | ||
99 | |||
100 | if (this.embed.player.webtorrent) { | ||
101 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) | ||
102 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
103 | } | ||
104 | } | ||
105 | |||
106 | private loadWebTorrentResolutions () { | ||
107 | const resolutions = [] | ||
108 | const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() | ||
109 | |||
110 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { | ||
111 | let label = videoFile.resolution.label | ||
112 | if (videoFile.fps && videoFile.fps >= 50) { | ||
113 | label += videoFile.fps | ||
114 | } | ||
115 | |||
116 | resolutions.push({ | ||
117 | id: videoFile.resolution.id, | ||
118 | label, | ||
119 | src: videoFile.magnetUri, | ||
120 | active: videoFile.resolution.id === currentResolutionId | ||
121 | }) | ||
122 | } | ||
123 | |||
124 | this.resolutions = resolutions | ||
125 | this.channel.notify({ | ||
126 | method: 'resolutionUpdate', | ||
127 | params: this.resolutions | ||
128 | }) | ||
129 | } | ||
130 | } | ||
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index c3b6e08ca..5a15bf552 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -11,7 +11,7 @@ | |||
11 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> | 11 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> |
12 | </head> | 12 | </head> |
13 | 13 | ||
14 | <body> | 14 | <body id="custom-css"> |
15 | 15 | ||
16 | <div id="error-block"> | 16 | <div id="error-block"> |
17 | <h1 id="error-title"></h1> | 17 | <h1 id="error-title"></h1> |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 707f04253..cfe8e94b1 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -1,9 +1,6 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | 2 | ||
3 | import * as Channel from 'jschannel' | ||
4 | |||
5 | import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared' | 3 | import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared' |
6 | import { PeerTubeResolution } from '../player/definitions' | ||
7 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' | 4 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' |
8 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 5 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
9 | import { | 6 | import { |
@@ -13,133 +10,9 @@ import { | |||
13 | PlayerMode | 10 | PlayerMode |
14 | } from '../../assets/player/peertube-player-manager' | 11 | } from '../../assets/player/peertube-player-manager' |
15 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | 12 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' |
13 | import { PeerTubeEmbedApi } from './embed-api' | ||
16 | 14 | ||
17 | /** | 15 | export class PeerTubeEmbed { |
18 | * Embed API exposes control of the embed player to the outside world via | ||
19 | * JSChannels and window.postMessage | ||
20 | */ | ||
21 | class PeerTubeEmbedApi { | ||
22 | private channel: Channel.MessagingChannel | ||
23 | private isReady = false | ||
24 | private resolutions: PeerTubeResolution[] = null | ||
25 | |||
26 | constructor (private embed: PeerTubeEmbed) { | ||
27 | } | ||
28 | |||
29 | initialize () { | ||
30 | this.constructChannel() | ||
31 | this.setupStateTracking() | ||
32 | |||
33 | // We're ready! | ||
34 | |||
35 | this.notifyReady() | ||
36 | } | ||
37 | |||
38 | private get element () { | ||
39 | return this.embed.videoElement | ||
40 | } | ||
41 | |||
42 | private constructChannel () { | ||
43 | const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) | ||
44 | |||
45 | channel.bind('play', (txn, params) => this.embed.player.play()) | ||
46 | channel.bind('pause', (txn, params) => this.embed.player.pause()) | ||
47 | channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) | ||
48 | channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) | ||
49 | channel.bind('getVolume', (txn, value) => this.embed.player.volume()) | ||
50 | channel.bind('isReady', (txn, params) => this.isReady) | ||
51 | channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) | ||
52 | channel.bind('getResolutions', (txn, params) => this.resolutions) | ||
53 | channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) | ||
54 | channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) | ||
55 | channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) | ||
56 | |||
57 | this.channel = channel | ||
58 | } | ||
59 | |||
60 | private setResolution (resolutionId: number) { | ||
61 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return | ||
62 | |||
63 | // Auto resolution | ||
64 | if (resolutionId === -1) { | ||
65 | this.embed.player.webtorrent().enableAutoResolution() | ||
66 | return | ||
67 | } | ||
68 | |||
69 | this.embed.player.webtorrent().disableAutoResolution() | ||
70 | this.embed.player.webtorrent().updateResolution(resolutionId) | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Let the host know that we're ready to go! | ||
75 | */ | ||
76 | private notifyReady () { | ||
77 | this.isReady = true | ||
78 | this.channel.notify({ method: 'ready', params: true }) | ||
79 | } | ||
80 | |||
81 | private setupStateTracking () { | ||
82 | let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted' | ||
83 | |||
84 | setInterval(() => { | ||
85 | const position = this.element.currentTime | ||
86 | const volume = this.element.volume | ||
87 | |||
88 | this.channel.notify({ | ||
89 | method: 'playbackStatusUpdate', | ||
90 | params: { | ||
91 | position, | ||
92 | volume, | ||
93 | playbackState: currentState | ||
94 | } | ||
95 | }) | ||
96 | }, 500) | ||
97 | |||
98 | this.element.addEventListener('play', ev => { | ||
99 | currentState = 'playing' | ||
100 | this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) | ||
101 | }) | ||
102 | |||
103 | this.element.addEventListener('pause', ev => { | ||
104 | currentState = 'paused' | ||
105 | this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) | ||
106 | }) | ||
107 | |||
108 | // PeerTube specific capabilities | ||
109 | |||
110 | if (this.embed.player.webtorrent) { | ||
111 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) | ||
112 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | private loadWebTorrentResolutions () { | ||
117 | const resolutions = [] | ||
118 | const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() | ||
119 | |||
120 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { | ||
121 | let label = videoFile.resolution.label | ||
122 | if (videoFile.fps && videoFile.fps >= 50) { | ||
123 | label += videoFile.fps | ||
124 | } | ||
125 | |||
126 | resolutions.push({ | ||
127 | id: videoFile.resolution.id, | ||
128 | label, | ||
129 | src: videoFile.magnetUri, | ||
130 | active: videoFile.resolution.id === currentResolutionId | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | this.resolutions = resolutions | ||
135 | this.channel.notify({ | ||
136 | method: 'resolutionUpdate', | ||
137 | params: this.resolutions | ||
138 | }) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | class PeerTubeEmbed { | ||
143 | videoElement: HTMLVideoElement | 16 | videoElement: HTMLVideoElement |
144 | player: any | 17 | player: any |
145 | playerOptions: any | 18 | playerOptions: any |
@@ -152,6 +25,12 @@ class PeerTubeEmbed { | |||
152 | enableApi = false | 25 | enableApi = false |
153 | startTime: number | string = 0 | 26 | startTime: number | string = 0 |
154 | stopTime: number | string | 27 | stopTime: number | string |
28 | |||
29 | title: boolean | ||
30 | warningTitle: boolean | ||
31 | bigPlayBackgroundColor: string | ||
32 | foregroundColor: string | ||
33 | |||
155 | mode: PlayerMode | 34 | mode: PlayerMode |
156 | scope = 'peertube' | 35 | scope = 'peertube' |
157 | 36 | ||
@@ -245,13 +124,18 @@ class PeerTubeEmbed { | |||
245 | this.controls = this.getParamToggle(params, 'controls', true) | 124 | this.controls = this.getParamToggle(params, 'controls', true) |
246 | this.muted = this.getParamToggle(params, 'muted', false) | 125 | this.muted = this.getParamToggle(params, 'muted', false) |
247 | this.loop = this.getParamToggle(params, 'loop', false) | 126 | this.loop = this.getParamToggle(params, 'loop', false) |
127 | this.title = this.getParamToggle(params, 'title', true) | ||
248 | this.enableApi = this.getParamToggle(params, 'api', this.enableApi) | 128 | this.enableApi = this.getParamToggle(params, 'api', this.enableApi) |
129 | this.warningTitle = this.getParamToggle(params, 'warningTitle', true) | ||
249 | 130 | ||
250 | this.scope = this.getParamString(params, 'scope', this.scope) | 131 | this.scope = this.getParamString(params, 'scope', this.scope) |
251 | this.subtitle = this.getParamString(params, 'subtitle') | 132 | this.subtitle = this.getParamString(params, 'subtitle') |
252 | this.startTime = this.getParamString(params, 'start') | 133 | this.startTime = this.getParamString(params, 'start') |
253 | this.stopTime = this.getParamString(params, 'stop') | 134 | this.stopTime = this.getParamString(params, 'stop') |
254 | 135 | ||
136 | this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor') | ||
137 | this.foregroundColor = this.getParamString(params, 'foregroundColor') | ||
138 | |||
255 | this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' | 139 | this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' |
256 | } catch (err) { | 140 | } catch (err) { |
257 | console.error('Cannot get params from URL.', err) | 141 | console.error('Cannot get params from URL.', err) |
@@ -276,15 +160,7 @@ class PeerTubeEmbed { | |||
276 | } | 160 | } |
277 | 161 | ||
278 | const videoInfo: VideoDetails = await videoResponse.json() | 162 | const videoInfo: VideoDetails = await videoResponse.json() |
279 | let videoCaptions: VideoJSCaption[] = [] | 163 | const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) |
280 | if (captionsResponse.ok) { | ||
281 | const { data } = (await captionsResponse.json()) as ResultList<VideoCaption> | ||
282 | videoCaptions = data.map(c => ({ | ||
283 | label: peertubeTranslate(c.language.label, serverTranslations), | ||
284 | language: c.language.id, | ||
285 | src: window.location.origin + c.captionPath | ||
286 | })) | ||
287 | } | ||
288 | 164 | ||
289 | this.loadParams() | 165 | this.loadParams() |
290 | 166 | ||
@@ -337,33 +213,66 @@ class PeerTubeEmbed { | |||
337 | } | 213 | } |
338 | 214 | ||
339 | this.player = await PeertubePlayerManager.initialize(this.mode, options) | 215 | this.player = await PeertubePlayerManager.initialize(this.mode, options) |
340 | |||
341 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) | 216 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) |
342 | 217 | ||
343 | window[ 'videojsPlayer' ] = this.player | 218 | window[ 'videojsPlayer' ] = this.player |
344 | 219 | ||
220 | this.buildCSS() | ||
221 | |||
222 | await this.buildDock(videoInfo, configResponse) | ||
223 | |||
224 | this.initializeApi() | ||
225 | } | ||
226 | |||
227 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | ||
228 | if (err.message.indexOf('from xs param') !== -1) { | ||
229 | this.player.dispose() | ||
230 | this.videoElement = null | ||
231 | this.displayError('This video is not available because the remote instance is not responding.', translations) | ||
232 | return | ||
233 | } | ||
234 | } | ||
235 | |||
236 | private async buildDock (videoInfo: VideoDetails, configResponse: Response) { | ||
345 | if (this.controls) { | 237 | if (this.controls) { |
238 | const title = this.title ? videoInfo.name : undefined | ||
239 | |||
346 | const config: ServerConfig = await configResponse.json() | 240 | const config: ServerConfig = await configResponse.json() |
347 | const description = config.tracker.enabled | 241 | const description = config.tracker.enabled && this.warningTitle |
348 | ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>' | 242 | ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>' |
349 | : undefined | 243 | : undefined |
350 | 244 | ||
351 | this.player.dock({ | 245 | this.player.dock({ |
352 | title: videoInfo.name, | 246 | title, |
353 | description | 247 | description |
354 | }) | 248 | }) |
355 | } | 249 | } |
250 | } | ||
356 | 251 | ||
357 | this.initializeApi() | 252 | private buildCSS () { |
253 | const body = document.getElementById('custom-css') | ||
254 | |||
255 | if (this.bigPlayBackgroundColor) { | ||
256 | body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor) | ||
257 | } | ||
258 | |||
259 | if (this.foregroundColor) { | ||
260 | body.style.setProperty('--embedForegroundColor', this.foregroundColor) | ||
261 | } | ||
358 | } | 262 | } |
359 | 263 | ||
360 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | 264 | private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise<VideoJSCaption[]> { |
361 | if (err.message.indexOf('from xs param') !== -1) { | 265 | if (captionsResponse.ok) { |
362 | this.player.dispose() | 266 | const { data } = (await captionsResponse.json()) as ResultList<VideoCaption> |
363 | this.videoElement = null | 267 | |
364 | this.displayError('This video is not available because the remote instance is not responding.', translations) | 268 | return data.map(c => ({ |
365 | return | 269 | label: peertubeTranslate(c.language.label, serverTranslations), |
270 | language: c.language.id, | ||
271 | src: window.location.origin + c.captionPath | ||
272 | })) | ||
366 | } | 273 | } |
274 | |||
275 | return [] | ||
367 | } | 276 | } |
368 | } | 277 | } |
369 | 278 | ||